Skip to content

Conversation

@dsarno
Copy link
Collaborator

@dsarno dsarno commented Dec 31, 2025

This branch makes MCP’s HTTP workflow much easier to use and harder to break by:

  • making transport choice explicit (HTTP local vs HTTP remote vs stdio),
  • improving lifecycle semantics (start/stop the right thing, avoid duplicate sessions),
  • reducing UI stalls from CLI status checks (Claude Code).

What changed (high level)

  • Transport selection is now first-class in the UI

    • New Transport dropdown with HTTP Local / HTTP Remote / Stdio.
    • Stored via prefs (UseHttpTransport + HttpTransportScope), with back-compat inference if the scope pref doesn’t exist yet.
  • Local HTTP server UX is consolidated + more reliable

    • HTTP Local shows a manual “launch server command” (copyable) and a single “Start Server / Stop Server” button.

    • Clicking Start Server launches the local server and then auto-starts the session, waiting for the port to become reachable to reduce transient “start failed then recovers” behavior.

    • Clicking Stop Server ends the session and attempts to stop the local server.

    • Start generates a pidfile + per-launch token and launches the server with --pidfile + --unity-instance-token.

    • Stop prefers the handshake path: pidfile → validate PID on port → validate token → kill, then cleans up.

    • Reduces reliance on “port scan + heuristics” and avoids “Refusing to stop…” false negatives.

  • Safer lifecycle semantics across transports

    • When the user switches transport, the system best-effort stops the deselected transport to avoid duplicated sessions (e.g. stdio lingering when switching to HTTP).
    • On editor quit, best-effort cleanup stops transports; if HTTP Local is selected, it also attempts to stop the local server.
  • Claude Code client setup improvements

    • Non-blocking CLI status checks to avoid UI stalls/repaint delays.
    • More robust parsing of Claude CLI output; improved handling of transport mismatch (HTTP vs stdio).
    • Re-register flow removes any existing UnityMCP entry first to ensure clean, current registration.

User-facing UI polish

  • Clearer “Health” semantics (ping/verify can differ from session state).
  • URL field behavior improved (persist/normalize on focus-out / Enter, not every keystroke).
  • Better layout/styling for the consolidated Start/Stop button (including a “danger” style when stopping).

Implementation notes

  • Unity Editor
    • New/updated prefs in EditorPrefKeys (including scope + handshake storage).
    • Core changes in McpConnectionSection, BridgeControlService, ServerManagementService, plus shutdown cleanup.
  • Python server
    • Adds CLI args: --pidfile, --unity-instance-token and writes the pidfile on startup.

Testing

  • Python: pytest ✅ (83 passed, 2 skipped, 7 xpassed)
  • Manual (Unity): verified pidfile creation and deterministic stop on HTTP Local.

Files touched (major)

  • MCPForUnity/Editor/Windows/Components/Connection/McpConnectionSection.*
  • MCPForUnity/Editor/Services/ServerManagementService.cs
  • MCPForUnity/Editor/Services/BridgeControlService.cs
  • MCPForUnity/Editor/Services/McpEditorShutdownCleanup.cs
  • MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs
  • Server/src/main.py

Summary by Sourcery

Overhaul HTTP transport selection and local server lifecycle to make MCP sessions safer, less error‑prone, and better integrated with Claude Code, while adding deterministic shutdown and editor quit cleanup.

New Features:

  • Add an HTTP transport scope preference with distinct HTTP Local vs HTTP Remote modes, driving both UI behavior and local server management.
  • Expose a best-effort API to detect whether a local MCP HTTP server is running on the configured local URL/port for driving editor UI state.
  • Extend the Python MCP server with optional --pidfile and --unity-instance-token arguments so Unity can robustly identify and manage Unity-started server processes.

Bug Fixes:

  • Reduce false negatives and accidental kills when stopping local HTTP servers by tightening process detection, avoiding Unity/Hub PIDs, and improving diagnostics when stop heuristics do not match.
  • Prevent UI stalls from Claude CLI status checks by moving Claude Code registration inspection to background tasks that use a thread-safe status method and captured editor settings.
  • Avoid duplicated MCP sessions when switching transports by best-effort stopping the deselected transport before starting the new one.

Enhancements:

  • Introduce explicit transport modes (HTTP Local, HTTP Remote, Stdio) with persisted scope and UI that adapts health, controls, and layout based on selection.
  • Consolidate local HTTP server controls into a single Start/Stop button that can also auto-start MCP sessions after the server becomes reachable, with better URL handling and command display limited to localhost URLs.
  • Implement deterministic tracking and shutdown of Unity-launched local HTTP servers via pidfiles, per-launch tokens, PID fingerprints, and safer process heuristics across platforms, plus log tailing helpers.
  • Ensure transports are treated as mutually exclusive by stopping the previously selected transport (and legacy stdio host) on start, and by cleaning up transports and local HTTP servers on Unity editor quit.
  • Improve terminal launch behavior on macOS/Windows by generating helper scripts instead of using AppleScript/nested shells, and by enriching process termination with graceful shutdown and verification on Unix.
  • Refine connection health UI so health checks are decoupled from session state, hidden outside debug mode when healthy, and so stdio end-session actions are visually emphasized.

Summary by CodeRabbit

  • New Features

    • Local vs. remote HTTP transport selection, plus deterministic local-server start/stop and managed-server stop detection
    • Instance-token and pidfile support for launched servers
    • Thread-safe background status checks and faster client refresh when transport changes
    • Auto-start MCP session after server becomes reachable
  • Bug Fixes

    • Improved editor shutdown cleanup and mutually-exclusive transport switching
    • More robust process stop/diagnostics and retry-friendly command handling
  • UI Improvements

    • Consolidated server start/stop controls, refined debug label, and server-running visual states

✏️ Tip: You can customize this high-level summary in your review settings.

…stering

When registering with Claude Code, if a UnityMCP server already exists,
remove it first before adding the new registration. This ensures the
transport mode (HTTP vs stdio) is always updated to match the current
UseHttpTransport EditorPref setting.

Previously, if a stdio registration existed and the user tried to register
with HTTP, the command would fail with 'already exists' and the old stdio
configuration would remain unchanged.
…ctly

The validation code was incorrectly parsing the output of 'claude mcp get UnityMCP' by looking for JSON format ("transport": "http"), but the CLI actually returns human-readable text format ("Type: http"). This caused the transport mismatch detection to never trigger, allowing stdio to be selected in the UI while HTTP was registered with Claude Code.

Changes:
- Fix parsing logic to check for "Type: http" or "Type: stdio" in CLI output
- Add OnTransportChanged event to refresh client status when transport changes
- Wire up event handler to trigger client status refresh on transport dropdown change

This ensures that when the transport mode in Unity doesn't match what's registered with Claude Code, the UI will correctly show an error status with instructions to re-register.
…laude CLI status checks and HTTP simplifications
This commit resolves three issues with Claude Code registration:

1. UI blocking: Removed synchronous CheckStatus() call after registration
   that was blocking the editor. Status is now set immediately with async
   verification happening in the background.

2. Thread safety: Fixed "can only be called from the main thread" errors
   by capturing Application.dataPath and EditorPrefs.GetBool() on the main
   thread before spawning async status check tasks.

3. Transport mismatch detection: Transport mode changes now trigger immediate
   status checks to detect HTTP/stdio mismatches, instead of waiting for the
   45-second refresh interval.

The registration button now turns green immediately after successful
registration without blocking, and properly detects transport mismatches
when switching between HTTP and stdio modes.
Address code review feedback by making CheckStatusWithProjectDir thread-safe
by design rather than by convention:

1. Made projectDir and useHttpTransport parameters non-nullable to prevent
   accidental background thread calls without captured values

2. Removed nullable fallback to EditorPrefs.GetBool() which would cause
   thread safety violations if called from background threads

3. Added ArgumentNullException for null projectDir instead of falling back
   to Application.dataPath (which is main-thread only)

4. Added XML documentation clearly stating threading contracts:
   - CheckStatus() must be called from main thread
   - CheckStatusWithProjectDir() is safe for background threads

5. Removed unreachable else branch in async status check code

These changes make it impossible to misuse the API from background threads,
with compile-time enforcement instead of runtime errors.
@sourcery-ai
Copy link
Contributor

sourcery-ai bot commented Dec 31, 2025

Reviewer's Guide

Overhauls MCP’s HTTP transport and lifecycle handling: introduces explicit HTTP Local/HTTP Remote/Stdio transport selection with scoped prefs, adds deterministic management of Unity-launched local HTTP servers via pidfiles + instance tokens, refines connection/health UI and consolidated Start/Stop behavior, tightens shutdown cleanup, and makes Claude CLI integration non-blocking and transport-aware, including server-side support for lifecycle tokens via new CLI args.

Sequence diagram for HTTP Local Start Server and deterministic lifecycle

sequenceDiagram
    actor User
    participant McpConnectionSection
    participant BridgeControlService
    participant TransportManager
    participant ServerManagementService
    participant PythonServerMain as PythonServer_main_py

    User->>McpConnectionSection: Click Start Server (HTTP Local)
    McpConnectionSection->>ServerManagementService: StartLocalHttpServer()
    ServerManagementService->>ServerManagementService: StopLocalHttpServerInternal(quiet=true)
    ServerManagementService->>ServerManagementService: Check port free via GetListeningProcessIdsForPort
    alt port in use
        ServerManagementService-->>McpConnectionSection: return false
        McpConnectionSection-->>User: Show Port In Use dialog
    else port free
        ServerManagementService->>ServerManagementService: Create pidfile path and instance token
        ServerManagementService->>ServerManagementService: StoreLocalHttpServerHandshake(pidfile, token)
        ServerManagementService->>PythonServerMain: Launch terminal process with --pidfile and --unity-instance-token
        PythonServerMain->>PythonServerMain: Parse args and write pidfile with os.getpid()
        PythonServerMain->>PythonServerMain: Set UNITY_MCP_INSTANCE_TOKEN env var
        ServerManagementService-->>McpConnectionSection: return true
        McpConnectionSection->>McpConnectionSection: TryAutoStartSessionAfterHttpServerAsync()
        McpConnectionSection->>McpConnectionSection: WaitForHttpServerAcceptingConnectionsAsync(timeout)
        loop until reachable or timeout
            McpConnectionSection->>PythonServerMain: TcpClient.ConnectAsync(host, port)
            PythonServerMain-->>McpConnectionSection: accept or fail
        end
        McpConnectionSection->>BridgeControlService: StartAsync()
        BridgeControlService->>TransportManager: StopAsync(TransportMode.Stdio)
        BridgeControlService->>TransportManager: StartAsync(TransportMode.Http)
        TransportManager-->>BridgeControlService: started bool
        alt started
            BridgeControlService-->>McpConnectionSection: true
            McpConnectionSection->>McpConnectionSection: VerifyBridgeConnectionAsync()
            McpConnectionSection-->>User: Session running, Health updated
        else not started
            BridgeControlService-->>McpConnectionSection: false
            McpConnectionSection-->>User: Warn failed to auto start session
        end
    end
Loading

Sequence diagram for HTTP Local Stop Server with pidfile and token validation

sequenceDiagram
    actor User
    participant McpConnectionSection
    participant BridgeControlService
    participant ServerManagementService
    participant PythonServerMain as PythonServer_main_py

    User->>McpConnectionSection: Click Stop Server (HTTP Local)
    alt session running
        McpConnectionSection->>BridgeControlService: StopAsync()
        BridgeControlService->>TransportManager: StopAsync(currentMode)
        BridgeControlService-->>McpConnectionSection: completed
    end
    McpConnectionSection->>ServerManagementService: StopLocalHttpServer()
    ServerManagementService->>ServerManagementService: TryGetLocalHttpServerHandshake(out pidfile, out token)
    alt handshake exists
        ServerManagementService->>ServerManagementService: TryReadPidFromPidFile(pidfile)
        alt pid invalid
            ServerManagementService-->>McpConnectionSection: false (pidfile not ready)
        else pid valid
            ServerManagementService->>ServerManagementService: GetListeningProcessIdsForPort(port)
            alt no listener
                ServerManagementService->>ServerManagementService: Delete pidfile and ClearLocalServerPidTracking()
                ServerManagementService-->>McpConnectionSection: false
            else listener present
                ServerManagementService->>ServerManagementService: ProcessCommandLineContainsInstanceToken(pid, token)
                alt token and port match
                    ServerManagementService->>PythonServerMain: TerminateProcess(pid)
                    PythonServerMain-->>ServerManagementService: process exits
                    ServerManagementService->>ServerManagementService: Delete pidfile and ClearLocalServerPidTracking()
                    ServerManagementService-->>McpConnectionSection: true
                else validation failed
                    ServerManagementService-->>McpConnectionSection: false (refusing to stop)
                end
            end
        end
    else no handshake
        ServerManagementService->>ServerManagementService: TryGetStoredLocalServerPid(port, out storedPid)
        ServerManagementService->>ServerManagementService: GetListeningProcessIdsForPort(port)
        ServerManagementService->>ServerManagementService: LooksLikeMcpServerProcess(pid)
        alt looks like MCP server
            ServerManagementService->>PythonServerMain: TerminateProcess(pid)
            PythonServerMain-->>ServerManagementService: exited
            ServerManagementService-->>McpConnectionSection: true or false
        else not MCP server
            ServerManagementService-->>McpConnectionSection: false (refusing to stop)
        end
    end
    McpConnectionSection->>McpConnectionSection: UpdateConnectionStatus()
    McpConnectionSection-->>User: UI reflects stopped state
Loading

Sequence diagram for non-blocking Claude CLI status check with transport-aware validation

sequenceDiagram
    actor User
    participant MCPForUnityEditorWindow as EditorWindow
    participant McpClientConfigSection
    participant ClaudeCliMcpConfigurator
    participant ClaudeCliCli as ClaudeCLI

    User->>EditorWindow: Open MCP For Unity window
    EditorWindow->>McpClientConfigSection: RefreshSelectedClient(forceImmediate=true)
    McpClientConfigSection->>McpClientConfigSection: RefreshClientStatus(selected, forceImmediate)
    McpClientConfigSection->>McpClientConfigSection: RefreshClaudeCliStatus(client, forceImmediate)
    alt forceImmediate or needsRefresh
        McpClientConfigSection->>McpClientConfigSection: ApplyStatusToUi(showChecking=true)
        McpClientConfigSection->>McpClientConfigSection: Capture projectDir and useHttpTransport on main thread
        McpClientConfigSection->>ClaudeCliMcpConfigurator: CheckStatusWithProjectDir(projectDir, useHttpTransport, attemptAutoRewrite=false) async on background Task
        ClaudeCliMcpConfigurator->>ClaudeCLI: ExecPath.TryRun(claude, "mcp list")
        ClaudeCLI-->>ClaudeCliMcpConfigurator: stdout with UnityMCP entry or not
        alt UnityMCP present
            ClaudeCliMcpConfigurator->>ClaudeCLI: ExecPath.TryRun(claude, "mcp get UnityMCP")
            ClaudeCLI-->>ClaudeCliMcpConfigurator: stdout with Type http or Type stdio
            ClaudeCliMcpConfigurator->>ClaudeCliMcpConfigurator: Compare registered transport vs useHttpTransport
            alt mismatch
                ClaudeCliMcpConfigurator->>ClaudeCliMcpConfigurator: client.SetStatus(McpStatus.Error, transport mismatch message)
            else match
                ClaudeCliMcpConfigurator->>ClaudeCliMcpConfigurator: client.SetStatus(McpStatus.Configured)
            end
        else UnityMCP not present
            ClaudeCliMcpConfigurator->>ClaudeCliMcpConfigurator: client.SetStatus(McpStatus.NotConfigured)
        end
        ClaudeCliMcpConfigurator-->>McpClientConfigSection: completed Task
        McpClientConfigSection->>McpClientConfigSection: lastStatusChecks[client] = DateTime.UtcNow
        McpClientConfigSection->>McpClientConfigSection: ApplyStatusToUi(client)
        McpClientConfigSection-->>User: UI shows up to date Claude CLI status
    end
Loading

Class diagram for updated transport and server lifecycle management

classDiagram
    class IServerManagementService {
        <<interface>>
        +bool StartLocalHttpServer()
        +bool StopLocalHttpServer()
        +bool IsLocalHttpServerRunning()
        +bool TryGetLocalHttpServerCommand(out string command, out string error)
    }

    class ServerManagementService {
        -static HashSet~int~ LoggedStopDiagnosticsPids
        -static object LocalHttpServerProcessLock
        -static Process LocalHttpServerProcess
        -static bool OpenedHttpServerLogViewerThisSession
        +bool StartLocalHttpServer()
        +bool StopLocalHttpServer()
        +bool IsLocalHttpServerRunning()
        +bool TryGetLocalHttpServerCommand(out string command, out string error)
        -bool StopLocalHttpServerInternal(bool quiet)
        -void TryCaptureAndStoreLocalServerPid()
        -void TryOpenLogInTerminal(string logPath)
        -ProcessStartInfo CreateLocalHttpServerProcessStartInfo(string fileName, string arguments)
        -bool ProcessCommandLineContainsInstanceToken(int pid, string instanceToken)
        -bool TryGetUnixProcessArgs(int pid, out string argsLower)
        -bool LooksLikeMcpServerProcess(int pid)
        -bool TerminateProcess(int pid)
        -bool ProcessExistsUnix(int pid)
        -void StoreLocalServerPidTracking(int pid, int port, string argsHash)
        -bool TryGetStoredLocalServerPid(int expectedPort, out int pid)
        -void ClearLocalServerPidTracking()
        -void StoreLocalHttpServerHandshake(string pidFilePath, string instanceToken)
        -bool TryGetLocalHttpServerHandshake(out string pidFilePath, out string instanceToken)
        -string GetLocalHttpServerPidFilePath(int port)
        -bool TryReadPidFromPidFile(string pidFilePath, out int pid)
        -string ComputeShortHash(string input)
        -void LogStopDiagnosticsOnce(int pid, string details)
        -string TrimForLog(string s)
        -ProcessStartInfo CreateTerminalProcessStartInfo(string command)
        -bool TryGetLocalHttpServerCommandParts(out string fileName, out string arguments, out string displayCommand, out string error)
    }

    class TransportProtocol {
        <<enumeration>>
        HTTPLocal
        HTTPRemote
        Stdio
    }

    class McpConnectionSection {
        -TransportProtocol transportDropdownValue
        -Button connectionToggleButton
        -Button startHttpServerButton
        -Button testConnectionButton
        -VisualElement healthIndicator
        -VisualElement healthRow
        -Label healthStatusLabel
        -bool connectionToggleInProgress
        -bool autoStartInProgress
        -bool httpServerToggleInProgress
        -double lastLocalServerRunningPollTime
        -bool lastLocalServerRunning
        +event Action OnManualConfigUpdateRequested
        +event Action OnTransportChanged
        +void InitializeUI()
        +void RegisterCallbacks()
        +void UpdateConnectionStatus()
        +void UpdateHttpServerCommandDisplay()
        +void RefreshHttpUi()
        +Task VerifyBridgeConnectionAsync()
        -void PersistHttpUrlFromField()
        -void UpdateHttpFieldVisibility()
        -bool IsHttpLocalSelected()
        -void UpdateStartHttpButtonState()
        -async void OnHttpServerToggleClicked()
        -async Task TryAutoStartSessionAfterHttpServerAsync()
        -static async Task~bool~ WaitForHttpServerAcceptingConnectionsAsync(TimeSpan timeout)
    }

    class BridgeControlService {
        -ITransportManager _transportManager
        +Task~bool~ StartAsync()
        +Task StopAsync()
        -TransportMode ResolvePreferredMode()
    }

    class McpEditorShutdownCleanup {
        <<static>>
        +static void OnEditorQuitting()
    }

    class ClaudeCliMcpConfigurator {
        +ClaudeCliMcpConfigurator(McpClient client)
        +override string GetConfigPath()
        +override McpStatus CheckStatus(bool attemptAutoRewrite)
        +internal McpStatus CheckStatusWithProjectDir(string projectDir, bool useHttpTransport, bool attemptAutoRewrite)
        -void Register()
        -void Unregister()
    }

    class McpClientConfigSection {
        -int selectedClientIndex
        -List~IMcpClientConfigurator~ configurators
        -Dictionary~IMcpClientConfigurator, DateTime~ lastStatusChecks
        -HashSet~IMcpClientConfigurator~ statusRefreshInFlight
        +void RefreshSelectedClient(bool forceImmediate)
        -void RefreshClientStatus(IMcpClientConfigurator client, bool forceImmediate)
        -void RefreshClaudeCliStatus(IMcpClientConfigurator client, bool forceImmediate)
    }

    class MCPForUnityEditorWindow {
        -McpConnectionSection connectionSection
        -McpClientConfigSection clientConfigSection
        +void CreateGUI()
    }

    class EditorPrefKeys {
        <<static>>
        +const string UseHttpTransport
        +const string HttpTransportScope
        +const string LastLocalHttpServerPid
        +const string LastLocalHttpServerPort
        +const string LastLocalHttpServerStartedUtc
        +const string LastLocalHttpServerPidArgsHash
        +const string LastLocalHttpServerPidFilePath
        +const string LastLocalHttpServerInstanceToken
    }

    class TransportManager {
        +Task~bool~ StartAsync(TransportMode mode)
        +Task StopAsync(TransportMode mode)
    }

    class McpServerMainPy {
        +def main()
    }

    IServerManagementService <|.. ServerManagementService
    McpConnectionSection ..> IServerManagementService : uses
    McpConnectionSection ..> BridgeControlService : uses via MCPServiceLocator
    BridgeControlService ..> TransportManager : uses
    McpEditorShutdownCleanup ..> TransportManager : uses
    McpEditorShutdownCleanup ..> IServerManagementService : uses
    MCPForUnityEditorWindow o--> McpConnectionSection
    MCPForUnityEditorWindow o--> McpClientConfigSection
    McpClientConfigSection ..> ClaudeCliMcpConfigurator : uses
    ClaudeCliMcpConfigurator ..> McpServerMainPy : configures HTTP or stdio
    ServerManagementService ..> McpServerMainPy : starts local HTTP server
Loading

File-Level Changes

Change Details Files
Deterministic lifecycle management and PID tracking for Unity-started local HTTP servers, with safer stop heuristics and optional log viewing.
  • Introduced pidfile and per-launch instance token handshake, persisted through new EditorPrefs keys, to uniquely identify Unity-managed HTTP servers.
  • Reworked StartLocalHttpServer to stop any existing server quietly, check for port occupancy, append --pidfile/--unity-instance-token to the launch command, clear stale state, and start the server via terminal scripts with improved PATH handling.
  • Implemented IsLocalHttpServerRunning, improved StopLocalHttpServer via StopLocalHttpServerInternal with a handshake-first shutdown path, stored PID/fingerprint fallback, stricter process classification, and quieter logging when requested.
  • Added utilities for computing short hashes of process args, reading pidfiles, tracking last-known PID/port/timestamp, interrogating process command lines on Windows and Unix, and robust Unix ps/kill wrappers including process existence checks and debug diagnostics.
  • Refactored CreateTerminalProcessStartInfo to avoid AppleScript, generate .command/.cmd launcher scripts, and standardized terminal behavior across platforms; added TryOpenLogInTerminal for optional log tailing in a separate terminal.
MCPForUnity/Editor/Services/ServerManagementService.cs
MCPForUnity/Editor/Constants/EditorPrefKeys.cs
MCPForUnity/Editor/Services/IServerManagementService.cs
Connection UI now treats transport selection as first-class (HTTP Local vs HTTP Remote vs Stdio), consolidates HTTP Local Start/Stop, and improves health and HTTP URL behavior.
  • Extended transport enum to HTTPLocal/HTTPRemote/Stdio, added HttpTransportScope preference with back-compat inference from URL, and wired transport changes to persist scope and notify client config to refresh.
  • Updated HTTP URL field to persist/normalize on focus-out or Enter instead of per-keystroke, and clarified Health semantics with tooltips and conditional visibility tied to debug mode and health state.
  • Replaced separate Start/Stop HTTP server buttons with a single Start/Stop Server toggle that coordinates local server and session lifecycle, including detecting running servers, disabling manual toggle when HTTP Local is in control, and styling stop states as “danger”.
  • Implemented auto-start behavior after launching the HTTP server by polling TCP reachability before starting the bridge, reducing transient failures, and polling/persisting local-server-running state to drive the Start/Stop label and enabled state.
  • Adjusted layout visibility so HTTP controls only show for non-stdio transports, local-server helpers only show for HTTP Local + localhost URLs, and connection toggle/test/health rows respect debug mode and selected transport.
MCPForUnity/Editor/Windows/Components/Connection/McpConnectionSection.cs
MCPForUnity/Editor/Windows/Components/Connection/McpConnectionSection.uxml
MCPForUnity/Editor/Windows/Components/Common.uss
MCPForUnity/Editor/Windows/Components/Settings/McpSettingsSection.uxml
MCPForUnity/Editor/Windows/MCPForUnityEditorWindow.cs
MCPForUnity/Editor/Constants/EditorPrefKeys.cs
Safer cross-transport lifecycle semantics and editor shutdown cleanup to avoid duplicate sessions and orphaned local servers.
  • In BridgeControlService.StartAsync, resolved the preferred transport and now proactively stops the opposite transport (including legacy stdio host) before starting the requested one.
  • Added McpEditorShutdownCleanup InitializeOnLoad hook that, on Editor quitting, best-effort stops both HTTP and Stdio transports with a bounded wait, then if HTTP Local is selected (based on scope or URL), attempts to stop the local HTTP server via ServerManagementService.
  • Extended MCP window wiring so connection section’s transport change event forces an immediate client status refresh, ensuring UI stays consistent with transport changes.
MCPForUnity/Editor/Services/BridgeControlService.cs
MCPForUnity/Editor/Services/McpEditorShutdownCleanup.cs
MCPForUnity/Editor/Services/McpEditorShutdownCleanup.cs.meta
MCPForUnity/Editor/Windows/MCPForUnityEditorWindow.cs
Claude Code (Claude CLI) integration made non-blocking and transport-aware, with safer status checking and registration flows.
  • Split ClaudeCliMcpConfigurator.CheckStatus into a main-thread wrapper and a thread-safe CheckStatusWithProjectDir that accepts projectDir and useHttpTransport, enabling background status checks without touching Unity-only APIs.
  • Enhanced status checking to parse claude mcp list and claude mcp get UnityMCP, detect whether UnityMCP is registered, and flag transport mismatches between registered type (http/stdio) and current UseHttpTransport setting with a clear error message.
  • Reworked registration to first attempt claude mcp get UnityMCP and, if present, remove it before re-adding with the current transport mode; removed the ‘already exists’ path, log success with explicit transport, and avoid synchronous CheckStatus immediately after registration.
  • Updated McpClientConfigSection to treat Claude CLI differently: route status refresh through a non-blocking background task that calls CheckStatusWithProjectDir using captured main-thread data, and adjusted RefreshSelectedClient semantics so non-Claude clients still refresh synchronously where needed.
MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs
MCPForUnity/Editor/Windows/Components/ClientConfig/McpClientConfigSection.cs
MCPForUnity/Editor/Constants/EditorPrefKeys.cs
Python HTTP server now supports Unity lifecycle handshakes via instance tokens and pidfiles.
  • Added CLI arguments --unity-instance-token and --pidfile to the FastMCP server, documenting their purpose for deterministic lifecycle management from Unity.
  • On startup, when these args are provided, the server exports UNITY_MCP_INSTANCE_TOKEN to the environment and writes its PID to the specified pidfile (creating directories as needed), logging a warning if pidfile writing fails.
  • Left existing HTTP URL/host/port environment wiring intact while layering the lifecycle handshake on top.
Server/src/main.py
Editor prefs and visibility tooling extended for new HTTP scope and server-tracking values.
  • Added EditorPrefKeys for HttpTransportScope and for last-known local HTTP server PID/port/start time/args hash/pidfile path/instance token to support deterministic lifecycle detection.
  • Exposed HttpTransportScope in the EditorPrefsWindow so it is visible/editable alongside existing MCP prefs.
MCPForUnity/Editor/Constants/EditorPrefKeys.cs
MCPForUnity/Editor/Windows/EditorPrefs/EditorPrefsWindow.cs

Possibly linked issues

  • #WSL compatibility request (Enable HTTP Server): Yes. The PR introduces HTTP Local/Remote transport and server lifecycle controls, fulfilling the WSL/remote connectivity request.

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 31, 2025

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

📝 Walkthrough

Walkthrough

Adds mutual exclusivity for HTTP vs stdio transports, comprehensive local HTTP server lifecycle (pidfile/token, start/stop, platform process handling), editor shutdown cleanup, centralized main-thread command dispatcher, Claude CLI thread-safe status checks, UI changes for local/remote HTTP, and Python server pidfile/token CLI options.

Changes

Cohort / File(s) Summary
Claude CLI Configuration
MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs
Added CheckStatusWithProjectDir(projectDir, useHttpTransport, claudePath, attemptAutoRewrite) for thread-safe status checks; changed registration to pre-clean existing UnityMCP, register with explicit transport, and set Configured on success.
Editor Prefs & Constants
MCPForUnity/Editor/Constants/EditorPrefKeys.cs, MCPForUnity/Editor/Windows/EditorPrefs/EditorPrefsWindow.cs
Added HttpTransportScope and six local HTTP server lifecycle keys (pidfile, port, startedUtc, args hash, pid file path, instance token); registered HttpTransportScope as a string pref.
Bridge Control & Transport Dispatcher
MCPForUnity/Editor/Services/BridgeControlService.cs, MCPForUnity/Editor/Services/Transport/TransportCommandDispatcher.cs
StartAsync now stops the "other" transport before starting requested transport; added InitializeOnLoad dispatcher, main-thread context capture, RequestMainThreadPump, and RunOnMainThreadAsync for safe main-thread command execution.
Server Management Interface & Implementation
MCPForUnity/Editor/Services/IServerManagementService.cs, MCPForUnity/Editor/Services/ServerManagementService.cs
Added IsLocalHttpServerRunning() and StopManagedLocalHttpServer() to interface; implemented robust local HTTP server lifecycle: handshake (pidfile/token), port checks, cross-platform process discovery/fingerprinting, deterministic start/stop with terminal launch scripts, and fallback heuristics.
Editor Shutdown Cleanup
MCPForUnity/Editor/Services/McpEditorShutdownCleanup.cs, .../McpEditorShutdownCleanup.cs.meta
New static class subscribed to EditorApplication.quitting to best-effort stop transports (concurrently, 750ms bound) and attempt local HTTP server shutdown with fallback to managed stop; guarded with try/catch and subscription dedupe.
Connection UI & Model
MCPForUnity/Editor/Windows/Components/Connection/McpConnectionSection.cs, .../McpConnectionSection.uxml
Split HTTP into HTTPLocal/HTTPRemote; added events OnManualConfigUpdateRequested and OnTransportChanged; unified server start/stop UI, added auto-start session polling after server launch, debounced URL normalization, health-row element, and UI visibility/guarding for server/session flows.
Client Config UI
MCPForUnity/Editor/Windows/Components/ClientConfig/McpClientConfigSection.cs
RefreshSelectedClient now accepts forceImmediate; captures main-thread-only values before background checks and routes Claude CLI checks through CheckStatusWithProjectDir to avoid Application.dataPath usage off-main-thread.
Main Editor Window
MCPForUnity/Editor/Windows/MCPForUnityEditorWindow.cs
Bound connectionSection.OnTransportChanged to call RefreshSelectedClient(forceImmediate: true) to refresh client status after transport changes.
Styling & Labels
MCPForUnity/Editor/Windows/Components/Common.uss, MCPForUnity/Editor/Windows/Components/Settings/McpSettingsSection.uxml
Added .start-server-button margin rule and .action-button.server-running state styling; replaced font-style with -unity-font-style in a couple places; changed label "Show Debug Logs:" → "Debug Mode:".
WebSocket / Main-thread usage
MCPForUnity/Editor/Services/Transport/Transports/WebSocketTransportClient.cs
Replaced custom main-thread delayCall logic with centralized TransportCommandDispatcher.RunOnMainThreadAsync for tool discovery retrieval, preserving cancellation semantics.
Python Server CLI & Runtime
Server/src/main.py, Server/src/services/tools/read_console.py
Added CLI options --unity-instance-token and --pidfile; token sets env var, pidfile writes PID on startup; read_console now accepts "all"/"*" for count and uses a capped default.
Server Transport & Plugin Hub
Server/src/transport/plugin_hub.py, Server/src/transport/unity_transport.py, Server/src/services/resources/editor_state.py
Added PluginDisconnectedError and per-session in-flight tracking; fast-fail command set and timeouts with retry hint responses; readiness probe for session windows; unity_transport now returns MCPResponse on exception; editor_state payload handling made defensive for missing data.

Sequence Diagram(s)

sequenceDiagram
    participant UI as McpConnectionSection (UI)
    participant Bridge as BridgeControlService
    participant Transports as Transport(s)
    participant Server as MCP Server

    UI->>Bridge: StartAsync(requestedTransport)
    rect rgb(220,240,255)
        Note over Bridge: Ensure mutual exclusivity
        Bridge->>Transports: Stop(otherTransport)
        Transports-->>Bridge: Stopped / Warning
    end
    rect rgb(240,220,255)
        Note over Bridge: Start requested transport
        Bridge->>Transports: Start(requestedTransport)
        Transports->>Server: Connect
        Server-->>Transports: Connected
        Transports-->>Bridge: Started
    end
    Bridge-->>UI: Completed
    UI->>UI: Raise OnTransportChanged
Loading
sequenceDiagram
    participant UI as McpConnectionSection
    participant ServerMgmt as ServerManagementService
    participant Terminal as Terminal/OS
    participant Server as Local HTTP Server
    participant Handshake as Handshake (pidfile/token)

    UI->>ServerMgmt: StartLocalHttpServer()
    ServerMgmt->>ServerMgmt: StopManagedLocalHttpServer() if present
    ServerMgmt->>ServerMgmt: Verify port availability
    ServerMgmt->>Handshake: Generate pidfile & token
    ServerMgmt->>Terminal: Launch command with --pidfile --unity-instance-token
    Terminal->>Server: Server process starts
    Server->>Handshake: Write pidfile
    Handshake-->>ServerMgmt: Confirmed
    ServerMgmt-->>UI: Started (persist handshake)
    UI->>UI: Poll server readiness -> success
    UI->>Bridge: Optionally auto-start session
Loading
sequenceDiagram
    participant Editor as Unity Editor
    participant Cleanup as McpEditorShutdownCleanup
    participant Bridge as BridgeControlService
    participant ServerMgmt as ServerManagementService

    Editor->>Cleanup: EditorApplication.quitting
    Cleanup->>Bridge: Stop HTTP & Stdio (concurrent, 750ms bound)
    Bridge-->>Cleanup: Stopped / Warnings
    Cleanup->>ServerMgmt: Attempt StopLocalHttpServer() or StopManagedLocalHttpServer()
    ServerMgmt-->>Cleanup: Attempted / Logged
    Cleanup->>Editor: Quit proceeds (errors logged)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐰
I hopped through transports, two by two,
Stopped the other, started the new;
Pidfiles, tokens — a tidy stash,
A main-thread pump with gentle dash;
Now editor quits with a soft, safe hush.

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 16.05% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main changes: introducing HTTP local/remote/stdio transport selection, safer lifecycle management, UI consolidation, and improved Claude Code integration.
✨ Finishing touches
  • 📝 Generate docstrings

📜 Recent review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1794038 and c1ed7f2.

📒 Files selected for processing (2)
  • MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs
  • MCPForUnity/Editor/Windows/Components/ClientConfig/McpClientConfigSection.cs
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2025-09-03T16:00:55.839Z
Learnt from: dsarno
Repo: CoplayDev/unity-mcp PR: 0
File: :0-0
Timestamp: 2025-09-03T16:00:55.839Z
Learning: ComponentResolver in UnityMcpBridge/Editor/Tools/ManageGameObject.cs is a nested static class within ManageGameObject, not a sibling type. The `using static MCPForUnity.Editor.Tools.ManageGameObject;` import is required to access ComponentResolver methods directly without the outer class qualifier.

Applied to files:

  • MCPForUnity/Editor/Windows/Components/ClientConfig/McpClientConfigSection.cs
📚 Learning: 2025-10-13T13:27:23.040Z
Learnt from: msanatan
Repo: CoplayDev/unity-mcp PR: 316
File: TestProjects/UnityMCPTests/Assets/Tests/EditMode/Resources.meta:1-8
Timestamp: 2025-10-13T13:27:23.040Z
Learning: UnityMcpBridge is a legacy project kept for backwards compatibility; MCPForUnity is the only active Unity plugin project. GUID collisions between UnityMcpBridge and MCPForUnity are acceptable.

Applied to files:

  • MCPForUnity/Editor/Windows/Components/ClientConfig/McpClientConfigSection.cs
📚 Learning: 2025-12-29T15:23:11.613Z
Learnt from: msanatan
Repo: CoplayDev/unity-mcp PR: 491
File: MCPForUnity/Editor/Windows/EditorPrefs/EditorPrefsWindow.cs:78-115
Timestamp: 2025-12-29T15:23:11.613Z
Learning: In MCPForUnity, prefer relying on the established testing process to catch UI initialization issues instead of adding defensive null checks for UI elements in editor windows. This means during reviews, verify that tests cover UI initialization paths and that code avoids repetitive null-check boilerplate in Editor Windows. If a UI element can be null, ensure there is a well-tested fallback or that its initialization is guaranteed by design, rather than sprinkling null checks throughout editor code.

Applied to files:

  • MCPForUnity/Editor/Windows/Components/ClientConfig/McpClientConfigSection.cs
  • MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs
🧬 Code graph analysis (2)
MCPForUnity/Editor/Windows/Components/ClientConfig/McpClientConfigSection.cs (5)
MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs (2)
  • ClaudeCliMcpConfigurator (328-612)
  • ClaudeCliMcpConfigurator (330-330)
MCPForUnity/Editor/Constants/EditorPrefKeys.cs (1)
  • EditorPrefKeys (7-56)
MCPForUnity/Editor/Services/MCPServiceLocator.cs (1)
  • MCPServiceLocator (11-91)
MCPForUnity/Editor/Services/PathResolverService.cs (1)
  • GetClaudeCliPath (47-103)
MCPForUnity/Editor/Windows/Components/Connection/McpConnectionSection.cs (4)
  • Task (608-660)
  • Task (662-749)
  • Task (751-761)
  • Task (763-828)
MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs (6)
MCPForUnity/Editor/Clients/IMcpClientConfigurator.cs (1)
  • McpStatus (30-30)
MCPForUnity/Editor/Constants/EditorPrefKeys.cs (1)
  • EditorPrefKeys (7-56)
MCPForUnity/Editor/Services/MCPServiceLocator.cs (1)
  • MCPServiceLocator (11-91)
MCPForUnity/Editor/Models/McpClient.cs (1)
  • SetStatus (42-54)
MCPForUnity/Editor/Helpers/ExecPath.cs (2)
  • ExecPath (12-272)
  • TryRun (161-227)
MCPForUnity/Editor/Helpers/McpLog.cs (4)
  • Error (48-51)
  • McpLog (7-52)
  • Warn (43-46)
  • Info (37-41)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Sourcery review
🔇 Additional comments (8)
MCPForUnity/Editor/Windows/Components/ClientConfig/McpClientConfigSection.cs (3)

9-9: LGTM!

The using directive is required for accessing EditorPrefKeys.UseHttpTransport on line 343.


292-303: LGTM!

The logic correctly differentiates between Claude CLI (expensive external process calls) and other configurators (cheap file I/O). Non-Claude CLI clients always refresh immediately, while Claude CLI respects the throttling unless explicitly forced.


336-381: LGTM!

The thread-safety pattern is correctly implemented:

  1. Main-thread values (projectDir, useHttpTransport, claudePath) are captured before Task.Run
  2. The thread-safe CheckStatusWithProjectDir receives these as explicit parameters
  3. The defensive type check on line 350 gracefully handles unexpected call sites
  4. Using attemptAutoRewrite: false is appropriate for background refresh to avoid silent config modifications
MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs (5)

337-349: LGTM!

The public CheckStatus method correctly captures main-thread-only values (Application.dataPath, EditorPrefs, GetClaudeCliPath) before delegating to the thread-safe internal method. The XML doc comment clearly warns callers about the main-thread requirement.


394-426: LGTM!

The transport mismatch detection is well-designed:

  • Correctly identifies both mismatch directions (HTTP↔stdio)
  • Provides an actionable error message guiding users to re-register
  • Gracefully handles cases where neither transport type is detected in the output
  • The warning log aids debugging

564-565: LGTM!

The comment clarifies the intentional omission of a CheckStatus call, aligning with the pattern established in Register.


408-409: No action required - code is compatible with project's minimum Unity version.

The project explicitly requires Unity 2021.3 LTS or newer (as documented in README.md and Server/README.md), which uses .NET Standard 2.1. The String.Contains(string, StringComparison) overload used at lines 408-409 is fully supported in .NET Standard 2.1 and does not require the IndexOf workaround suggested in the review.

Likely an incorrect or invalid review comment.


499-521: LGTM. The re-registration flow correctly ensures transport mode consistency by removing any existing registration before adding. The immediate status update avoids blocking a CheckStatus call. The pre-cleanup logic on line 501 properly relies on claude mcp get UnityMCP returning exit code 0 when the server exists and non-zero otherwise, which is the actual behavior of the Claude CLI.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 issue, and left some high level feedback:

  • In ProcessCommandLineContainsInstanceToken on Windows, the return !ok ? true : false; fallback makes a failed command-line query count as a token match, which seems opposite to the comment about falling back to port+pidfile validation; consider returning false (or distinguishing ok from tokenMatch) so failures don’t silently loosen the validation.
  • CreateLocalHttpServerProcessStartInfo in ServerManagementService is currently unused; either wire it into the HTTP-local launch path (e.g., for non-terminal launches or future refactors) or remove it to avoid dead code and confusion.
  • In RefreshClaudeCliStatus you unconditionally cast IMcpClientConfigurator to ClaudeCliMcpConfigurator; to make this more robust against future call sites, consider an as cast with a null-check (or constraining the method to the specific type) so an accidental non-Claude client doesn’t cause an InvalidCastException.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `ProcessCommandLineContainsInstanceToken` on Windows, the `return !ok ? true : false;` fallback makes a failed command-line query count as a token match, which seems opposite to the comment about falling back to port+pidfile validation; consider returning `false` (or distinguishing `ok` from `tokenMatch`) so failures don’t silently loosen the validation.
- `CreateLocalHttpServerProcessStartInfo` in `ServerManagementService` is currently unused; either wire it into the HTTP-local launch path (e.g., for non-terminal launches or future refactors) or remove it to avoid dead code and confusion.
- In `RefreshClaudeCliStatus` you unconditionally cast `IMcpClientConfigurator` to `ClaudeCliMcpConfigurator`; to make this more robust against future call sites, consider an `as` cast with a null-check (or constraining the method to the specific type) so an accidental non-Claude client doesn’t cause an `InvalidCastException`.

## Individual Comments

### Comment 1
<location> `MCPForUnity/Editor/Windows/Components/Connection/McpConnectionSection.cs:661-670` </location>
<code_context>
+                    return true; // Don't block if URL cannot be parsed
+                }
+
+                string host = uri.Host;
+                int port = uri.Port;
+
+                var deadline = DateTime.UtcNow + timeout;
</code_context>

<issue_to_address>
**suggestion (bug_risk):** Auto-connect polling may misbehave for wildcard/bind-all HTTP hosts like 0.0.0.0/::.

In `WaitForHttpServerAcceptingConnectionsAsync`, using `uri.Host` directly can break for wildcard hosts like `0.0.0.0` or `::`, where a `TcpClient` connect will usually fail even though the server is listening on loopback. That makes the readiness check too pessimistic and can hurt auto-start behavior. Consider normalizing wildcard/empty/`*` hosts to `localhost` (e.g., `127.0.0.1`) before attempting the TCP connection.

```suggestion
                string host = uri.Host;
                int port = uri.Port;

                // Normalize wildcard/bind-all hosts to loopback for readiness checks
                string normalizedHost;
                if (string.IsNullOrWhiteSpace(host) ||
                    string.Equals(host, "0.0.0.0", StringComparison.OrdinalIgnoreCase) ||
                    string.Equals(host, "::", StringComparison.OrdinalIgnoreCase) ||
                    string.Equals(host, "*", StringComparison.OrdinalIgnoreCase))
                {
                    normalizedHost = "localhost";
                }
                else
                {
                    normalizedHost = host;
                }

                var deadline = DateTime.UtcNow + timeout;
                while (DateTime.UtcNow < deadline)
                {
                    try
                    {
                        using var client = new TcpClient();
                        var connectTask = client.ConnectAsync(normalizedHost, port);
```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
MCPForUnity/Editor/Services/ServerManagementService.cs (1)

522-552: Consider removing unused method.

CreateLocalHttpServerProcessStartInfo appears to be unused after the refactor to terminal-based launching. If it's intentionally kept for potential future use, consider adding a comment explaining this.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b866a4c and 784dc8c.

📒 Files selected for processing (15)
  • MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs
  • MCPForUnity/Editor/Constants/EditorPrefKeys.cs
  • MCPForUnity/Editor/Services/BridgeControlService.cs
  • MCPForUnity/Editor/Services/IServerManagementService.cs
  • MCPForUnity/Editor/Services/McpEditorShutdownCleanup.cs
  • MCPForUnity/Editor/Services/McpEditorShutdownCleanup.cs.meta
  • MCPForUnity/Editor/Services/ServerManagementService.cs
  • MCPForUnity/Editor/Windows/Components/ClientConfig/McpClientConfigSection.cs
  • MCPForUnity/Editor/Windows/Components/Common.uss
  • MCPForUnity/Editor/Windows/Components/Connection/McpConnectionSection.cs
  • MCPForUnity/Editor/Windows/Components/Connection/McpConnectionSection.uxml
  • MCPForUnity/Editor/Windows/Components/Settings/McpSettingsSection.uxml
  • MCPForUnity/Editor/Windows/EditorPrefs/EditorPrefsWindow.cs
  • MCPForUnity/Editor/Windows/MCPForUnityEditorWindow.cs
  • Server/src/main.py
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2025-10-13T13:27:23.040Z
Learnt from: msanatan
Repo: CoplayDev/unity-mcp PR: 316
File: TestProjects/UnityMCPTests/Assets/Tests/EditMode/Resources.meta:1-8
Timestamp: 2025-10-13T13:27:23.040Z
Learning: UnityMcpBridge is a legacy project kept for backwards compatibility; MCPForUnity is the only active Unity plugin project. GUID collisions between UnityMcpBridge and MCPForUnity are acceptable.

Applied to files:

  • MCPForUnity/Editor/Services/McpEditorShutdownCleanup.cs.meta
  • MCPForUnity/Editor/Windows/Components/ClientConfig/McpClientConfigSection.cs
📚 Learning: 2025-12-29T15:23:11.613Z
Learnt from: msanatan
Repo: CoplayDev/unity-mcp PR: 491
File: MCPForUnity/Editor/Windows/EditorPrefs/EditorPrefsWindow.cs:78-115
Timestamp: 2025-12-29T15:23:11.613Z
Learning: In MCPForUnity, prefer relying on the established testing process to catch UI initialization issues instead of adding defensive null checks for UI elements in editor windows. This means during reviews, verify that tests cover UI initialization paths and that code avoids repetitive null-check boilerplate in Editor Windows. If a UI element can be null, ensure there is a well-tested fallback or that its initialization is guaranteed by design, rather than sprinkling null checks throughout editor code.

Applied to files:

  • MCPForUnity/Editor/Windows/EditorPrefs/EditorPrefsWindow.cs
  • MCPForUnity/Editor/Services/IServerManagementService.cs
  • MCPForUnity/Editor/Services/BridgeControlService.cs
  • MCPForUnity/Editor/Windows/Components/Connection/McpConnectionSection.cs
  • MCPForUnity/Editor/Windows/MCPForUnityEditorWindow.cs
  • MCPForUnity/Editor/Constants/EditorPrefKeys.cs
  • MCPForUnity/Editor/Windows/Components/ClientConfig/McpClientConfigSection.cs
  • MCPForUnity/Editor/Services/McpEditorShutdownCleanup.cs
  • MCPForUnity/Editor/Services/ServerManagementService.cs
  • MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs
📚 Learning: 2025-09-03T16:00:55.839Z
Learnt from: dsarno
Repo: CoplayDev/unity-mcp PR: 0
File: :0-0
Timestamp: 2025-09-03T16:00:55.839Z
Learning: ComponentResolver in UnityMcpBridge/Editor/Tools/ManageGameObject.cs is a nested static class within ManageGameObject, not a sibling type. The `using static MCPForUnity.Editor.Tools.ManageGameObject;` import is required to access ComponentResolver methods directly without the outer class qualifier.

Applied to files:

  • MCPForUnity/Editor/Windows/Components/ClientConfig/McpClientConfigSection.cs
  • MCPForUnity/Editor/Services/ServerManagementService.cs
🧬 Code graph analysis (7)
MCPForUnity/Editor/Windows/EditorPrefs/EditorPrefsWindow.cs (1)
MCPForUnity/Editor/Constants/EditorPrefKeys.cs (1)
  • EditorPrefKeys (7-56)
MCPForUnity/Editor/Services/IServerManagementService.cs (1)
MCPForUnity/Editor/Services/ServerManagementService.cs (1)
  • IsLocalHttpServerRunning (677-737)
MCPForUnity/Editor/Services/BridgeControlService.cs (2)
MCPForUnity/Editor/Helpers/McpLog.cs (2)
  • McpLog (7-52)
  • Warn (43-46)
MCPForUnity/Editor/Services/Transport/Transports/StdioBridgeHost.cs (2)
  • StdioBridgeHost (122-171)
  • Stop (415-484)
MCPForUnity/Editor/Windows/MCPForUnityEditorWindow.cs (1)
MCPForUnity/Editor/Windows/Components/ClientConfig/McpClientConfigSection.cs (1)
  • RefreshSelectedClient (292-303)
MCPForUnity/Editor/Windows/Components/ClientConfig/McpClientConfigSection.cs (2)
MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs (2)
  • ClaudeCliMcpConfigurator (328-613)
  • ClaudeCliMcpConfigurator (330-330)
MCPForUnity/Editor/Constants/EditorPrefKeys.cs (1)
  • EditorPrefKeys (7-56)
MCPForUnity/Editor/Services/McpEditorShutdownCleanup.cs (2)
MCPForUnity/Editor/Helpers/McpLog.cs (2)
  • McpLog (7-52)
  • Warn (43-46)
MCPForUnity/Editor/Services/IServerManagementService.cs (2)
  • IsLocalUrl (44-44)
  • StopLocalHttpServer (24-24)
MCPForUnity/Editor/Services/ServerManagementService.cs (3)
MCPForUnity/Editor/Constants/EditorPrefKeys.cs (1)
  • EditorPrefKeys (7-56)
MCPForUnity/Editor/Helpers/ExecPath.cs (1)
  • ExecPath (12-272)
MCPForUnity/Editor/Helpers/HttpEndpointUtility.cs (2)
  • HttpEndpointUtility (12-85)
  • GetBaseUrl (20-24)
🪛 Ruff (0.14.10)
Server/src/main.py

447-447: Do not catch blind exception: Exception

(BLE001)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Sourcery review
🔇 Additional comments (39)
MCPForUnity/Editor/Windows/Components/Settings/McpSettingsSection.uxml (1)

11-11: UI label clarification aligns well with PR's polish goals.

The change from "Show Debug Logs:" to "Debug Mode:" is a clear, concise improvement to the label that better communicates the setting's purpose. This fits well within the PR's broader UI/UX enhancements.

Server/src/main.py (2)

394-409: LGTM! Clear lifecycle management arguments.

The new CLI arguments are well-documented and appropriately optional, enabling Unity to coordinate server lifecycle without breaking existing usage patterns.


437-449: LGTM! Robust lifecycle handshake implementation.

The pidfile and token setup correctly handles edge cases (missing directories, varied path formats) and uses defensive exception handling appropriate for startup code. The broad exception catch flagged by Ruff (line 447) is intentional here—consistent with the pattern at lines 86-88—to ensure lifecycle setup never breaks server startup while still logging failures for debugging.

MCPForUnity/Editor/Services/IServerManagementService.cs (1)

26-30: LGTM: Well-documented interface addition.

The new IsLocalHttpServerRunning() method is appropriately scoped for UI state management with clear "best-effort" semantics documented. The implementation (from relevant snippets) shows proper fallback logic using handshake validation, PID checks, and port listener detection.

MCPForUnity/Editor/Services/BridgeControlService.cs (1)

85-101: LGTM: Proper transport mutual exclusion with appropriate error handling.

The mutual exclusion logic prevents duplicate sessions when switching transports. Error handling correctly logs warnings but allows startup to proceed, which is appropriate for best-effort cleanup. The legacy stdio stop provides defensive shutdown of any orphaned stdio transport state.

MCPForUnity/Editor/Windows/EditorPrefs/EditorPrefsWindow.cs (1)

54-54: LGTM: Consistent EditorPref type mapping.

The addition of HttpTransportScope to the known string preferences follows the established pattern and aligns with the new EditorPref key definition for transport scope management.

MCPForUnity/Editor/Services/McpEditorShutdownCleanup.cs.meta (1)

1-11: LGTM: Standard Unity metadata file.

This is a standard Unity .meta file containing MonoImporter metadata for the corresponding shutdown cleanup class. No functional changes to review.

MCPForUnity/Editor/Windows/MCPForUnityEditorWindow.cs (1)

178-179: LGTM: Appropriate reactive binding for transport changes.

The event binding ensures client configuration is immediately refreshed when the transport changes, maintaining UI consistency. The forceImmediate: true parameter ensures synchronous updates, which is appropriate for user-driven transport switching.

MCPForUnity/Editor/Windows/Components/Connection/McpConnectionSection.uxml (2)

22-22: LGTM: Consolidated server control button.

The replacement of multiple buttons with a single start-server-button aligns with the PR's goal of consolidating local HTTP server UX into a unified Start/Stop button. The CSS classes properly reference the styling defined in Common.uss.


36-36: LGTM: Named health row for programmatic access.

Adding name="health-row" to the health indicators section enables proper programmatic access for health status updates, consistent with the improved health verification workflow.

MCPForUnity/Editor/Constants/EditorPrefKeys.cs (1)

10-16: LGTM: Well-organized EditorPrefs keys for server lifecycle.

The seven new constants follow the established naming convention and properly support the local HTTP server lifecycle management introduced in this PR. The inline comment for HttpTransportScope usefully documents the expected values ("local" | "remote").

MCPForUnity/Editor/Windows/Components/Common.uss (3)

196-199: LGTM: Proper button alignment for server controls.

The start-server-button rule correctly aligns the button flush left (margin-left: 0px), consistent with other full-width controls in the manual config section.


201-212: LGTM: Appropriate danger styling for active server state.

The server-running state styling properly uses red-toned colors to indicate a running server/session, with appropriate hover and active states. This provides clear visual feedback that stopping the server is a significant action.


380-380: LGTM: Correct Unity UI Toolkit property.

The change from font-style: italic to -unity-font-style: italic is correct for Unity UI Toolkit, which requires the -unity- prefix for font styling properties.

MCPForUnity/Editor/Services/McpEditorShutdownCleanup.cs (3)

1-23: LGTM! Clean shutdown hook initialization.

The static constructor pattern with guard against duplicate subscriptions is appropriate for handling domain reloads. The empty catch block on line 21 is acceptable here since unsubscription failure is benign.


25-41: Transport shutdown with bounded wait looks correct.

The 750ms timeout for Task.WaitAll is reasonable for best-effort cleanup during editor quit. The inner empty catch block (line 35) appropriately swallows AggregateException from timeouts or task failures without blocking the quit process.


43-71: Local server cleanup logic with backward-compatible scope detection.

The fallback from explicit HttpTransportScope pref to IsLocalUrl() heuristics (lines 56-57) correctly maintains backward compatibility for users who haven't yet set the new scope preference.

MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs (4)

337-347: Good thread-safety pattern for main-thread value capture.

Capturing Application.dataPath and EditorPrefs values on the main thread before delegating to the thread-safe method is the correct approach for Unity editor code that needs to run background checks.


349-371: Thread-safe method with enforced contract.

The ArgumentNullException for projectDir (lines 368-371) correctly enforces the thread-safety contract at compile/runtime, preventing accidental fallback to Application.dataPath from background threads.


395-426: Transport mismatch detection improves UX.

Detecting mismatches between the registered transport mode and current setting, then providing an actionable error message ("Click Configure to re-register"), is a good user experience improvement. The case-insensitive Contains check for "Type: http" / "Type: stdio" is appropriate for CLI output parsing.


500-522: Pre-removal ensures clean registration state.

Removing existing UnityMCP registration before adding a new one (lines 500-510) ensures the transport mode is always up-to-date. Setting status to Configured immediately after successful registration (line 522) avoids blocking the UI thread with a verification call.

MCPForUnity/Editor/Windows/Components/ClientConfig/McpClientConfigSection.cs (2)

292-303: Parameter addition maintains backward compatibility.

The forceImmediate = false default preserves existing behavior for callers that don't pass the parameter. The logic at line 298 correctly forces immediate refresh for non-Claude CLI clients while respecting the parameter for Claude CLI.


336-377: Thread-safe async status refresh pattern.

The implementation correctly:

  1. Captures main-thread values (projectDir, useHttpTransport) before Task.Run (lines 341-343)
  2. Uses the thread-safe CheckStatusWithProjectDir method (line 350)
  3. Marshals UI updates back to main thread via EditorApplication.delayCall (line 363)

The cast at line 349 is safe due to the type check at line 307.

MCPForUnity/Editor/Windows/Components/Connection/McpConnectionSection.cs (7)

22-27: Transport enum expansion supports explicit local/remote distinction.

Splitting HTTP into HTTPLocal and HTTPRemote makes the transport mode explicit rather than relying solely on URL heuristics.


99-117: Backward-compatible transport initialization.

The scope inference from IsLocalUrl() when HttpTransportScope pref is empty (lines 111-114) maintains backward compatibility for existing users upgrading to this version.


165-181: Best-effort stop of deselected transport prevents duplicate sessions.

When switching between HTTP and Stdio, stopping the deselected transport (lines 171-180) is a good UX improvement. The fire-and-forget ContinueWith pattern is appropriate since failure to stop is non-critical.


184-194: URL field persistence on focus-out/Enter improves UX.

Not normalizing/overwriting on every keystroke (as noted in the comment) prevents fighting with user input and avoids duplicated scheme issues.


424-434: Throttled server status polling avoids performance issues.

Caching IsLocalHttpServerRunning results for 0.75s (line 428) prevents expensive port/PID checks on every UI tick, which could otherwise cause noticeable UI stalls.


597-649: Auto-start session with TCP readiness check.

Waiting for the HTTP server to accept TCP connections before attempting session start (lines 617-623) reduces transient "unable to connect" errors. The 10-attempt retry loop with 1s delay provides a reasonable 10s window for slower server startups.


651-691: TCP connection check for server readiness.

Using a raw TCP connect (lines 669-675) to detect server readiness is a lightweight and reliable approach. Returning true when URL can't be parsed (line 658) avoids blocking startup in edge cases.

MCPForUnity/Editor/Services/ServerManagementService.cs (9)

67-75: Centralized cleanup of EditorPrefs tracking keys.

Wrapping each DeleteKey in try-catch is defensive but acceptable given EditorPrefs can throw in edge cases.


168-204: Instance token validation reduces PID reuse risk.

Checking that the process command line contains the unique instance token (lines 176-198) provides strong validation that the PID hasn't been reused by an unrelated process. The fallback on line 193 (return !ok ? true : false) allows Stop to proceed if Windows can't read the command line due to permissions.


247-279: 6-hour PID validity window balances reliability and usability.

The timestamp-based validity check (lines 263-270) mitigates PID reuse risk while still allowing stop operations after domain reloads within a reasonable session window.


436-458: Pre-start port occupancy check prevents confusing failures.

Checking for existing listeners before launch (lines 445-456) and showing a clear dialog explaining why the server can't start is a good UX improvement over letting the launch fail with a cryptic error.


462-500: Per-launch pidfile and instance token enable deterministic stop.

Creating a unique instance token and pidfile path (lines 466-473) before launch, then storing them in EditorPrefs (line 499), enables the deterministic stop path that validates both PID and token before termination.


772-843: Deterministic handshake-based stop path is robust.

The handshake path validates:

  1. Pidfile exists and contains a valid PID (lines 778-787)
  2. PID is still listening on the expected port (line 812)
  3. Process command line contains the instance token (line 813)

Only when all validations pass does it terminate the process. This prevents accidentally killing unrelated processes that may have reused the PID.


903-930: Fingerprint matching with fallback for older installs.

The hash comparison (lines 916-918) provides strong validation, while the fallback to common server indicators (lines 923-929) maintains compatibility with servers started before this feature was added.


1189-1251: Graceful termination with escalation is appropriate.

The 8-second window for graceful shutdown (line 1214) before escalating to SIGKILL gives the server time to clean up while ensuring the stop operation eventually succeeds.


1360-1378: Script-based terminal launch avoids AppleScript permission prompts.

Using a .command script file opened via /usr/bin/open -a Terminal (macOS) avoids the automation permission prompts that AppleScript would trigger. This is a good cross-platform approach.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
MCPForUnity/Editor/Services/ServerManagementService.cs (1)

1098-1102: Critical bug: Unity safety check incorrectly blocks termination of MCP server.

The check s.Contains("unity") will match the MCP server's own arguments (e.g., "mcp-for-unity"), causing LooksLikeMcpServerProcess to return false for legitimate server processes. This prevents the "Stop Server" functionality from working correctly.

The check should exclude "mcp-for-unity" variants before testing for Unity Editor/Hub:

🔎 Proposed fix
+                    // Check for MCP server indicators first, before the Unity exclusion
+                    bool mentionsMcp = sCompact.Contains("mcp-for-unity")
+                                       || sCompact.Contains("mcp_for_unity")
+                                       || sCompact.Contains("mcpforunity");
+                    
+                    // If it explicitly mentions the server package/entrypoint, that is sufficient
+                    if (mentionsMcp)
+                    {
+                        return true;
+                    }
+
                     // Explicitly never kill Unity / Unity Hub processes
-                    if (s.Contains("unity") || s.Contains("unityhub") || s.Contains("unity hub"))
+                    // Note: Check after MCP indicators since "mcp-for-unity" contains "unity"
+                    if ((s.Contains("unity") && !mentionsMcp) || s.Contains("unityhub") || s.Contains("unity hub"))
                     {
                         return false;
                     }

                     // Positive indicators
                     bool mentionsUvx = s.Contains("uvx") || s.Contains(" uvx ");
                     bool mentionsUv = s.Contains("uv ") || s.Contains("/uv");
                     bool mentionsPython = s.Contains("python");
                     bool mentionsUvicorn = s.Contains("uvicorn");
-                    bool mentionsMcp = sCompact.Contains("mcp-for-unity")
-                                       || sCompact.Contains("mcp_for_unity")
-                                       || sCompact.Contains("mcpforunity");
                     bool mentionsTransport = sCompact.Contains("--transporthttp") || (sCompact.Contains("--transport") && sCompact.Contains("http"));
-
-                    // If it explicitly mentions the server package/entrypoint, that is sufficient
-                    // (we already only evaluate this for PIDs that are listening on our configured port).
-                    if (mentionsMcp)
-                    {
-                        return true;
-                    }
♻️ Duplicate comments (1)
MCPForUnity/Editor/Windows/Components/Connection/McpConnectionSection.cs (1)

651-706: Wildcard host normalization correctly addresses the previous review feedback.

The normalization of 0.0.0.0, ::, and * to localhost for TCP readiness checks is the correct approach, as servers bound to these addresses accept connections via loopback.

🧹 Nitpick comments (1)
MCPForUnity/Editor/Windows/Components/Connection/McpConnectionSection.cs (1)

165-181: Best-effort transport cleanup is reasonable, but async errors are silently swallowed.

The fire-and-forget pattern is intentional for best-effort cleanup. However, the empty ContinueWith(_ => { }) will silently swallow any async exceptions from StopAsync. Consider logging async failures for diagnosability:

🔎 Optional: Log async failures for diagnosability
                     try
                     {
                         var stopTask = MCPServiceLocator.TransportManager.StopAsync(stopMode);
-                        stopTask.ContinueWith(_ => { }, TaskScheduler.Default);
+                        stopTask.ContinueWith(t =>
+                        {
+                            if (t.IsFaulted)
+                            {
+                                McpLog.Warn($"Async stop of {stopMode} transport failed: {t.Exception?.GetBaseException()?.Message}");
+                            }
+                        }, TaskScheduler.Default);
                     }
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 784dc8c and 85932f6.

📒 Files selected for processing (3)
  • MCPForUnity/Editor/Services/ServerManagementService.cs
  • MCPForUnity/Editor/Windows/Components/ClientConfig/McpClientConfigSection.cs
  • MCPForUnity/Editor/Windows/Components/Connection/McpConnectionSection.cs
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2025-10-13T13:27:23.040Z
Learnt from: msanatan
Repo: CoplayDev/unity-mcp PR: 316
File: TestProjects/UnityMCPTests/Assets/Tests/EditMode/Resources.meta:1-8
Timestamp: 2025-10-13T13:27:23.040Z
Learning: UnityMcpBridge is a legacy project kept for backwards compatibility; MCPForUnity is the only active Unity plugin project. GUID collisions between UnityMcpBridge and MCPForUnity are acceptable.

Applied to files:

  • MCPForUnity/Editor/Windows/Components/Connection/McpConnectionSection.cs
  • MCPForUnity/Editor/Windows/Components/ClientConfig/McpClientConfigSection.cs
📚 Learning: 2025-09-03T16:00:55.839Z
Learnt from: dsarno
Repo: CoplayDev/unity-mcp PR: 0
File: :0-0
Timestamp: 2025-09-03T16:00:55.839Z
Learning: ComponentResolver in UnityMcpBridge/Editor/Tools/ManageGameObject.cs is a nested static class within ManageGameObject, not a sibling type. The `using static MCPForUnity.Editor.Tools.ManageGameObject;` import is required to access ComponentResolver methods directly without the outer class qualifier.

Applied to files:

  • MCPForUnity/Editor/Windows/Components/Connection/McpConnectionSection.cs
  • MCPForUnity/Editor/Services/ServerManagementService.cs
  • MCPForUnity/Editor/Windows/Components/ClientConfig/McpClientConfigSection.cs
📚 Learning: 2025-12-29T15:23:11.613Z
Learnt from: msanatan
Repo: CoplayDev/unity-mcp PR: 491
File: MCPForUnity/Editor/Windows/EditorPrefs/EditorPrefsWindow.cs:78-115
Timestamp: 2025-12-29T15:23:11.613Z
Learning: In MCPForUnity, prefer relying on the established testing process to catch UI initialization issues instead of adding defensive null checks for UI elements in editor windows. This means during reviews, verify that tests cover UI initialization paths and that code avoids repetitive null-check boilerplate in Editor Windows. If a UI element can be null, ensure there is a well-tested fallback or that its initialization is guaranteed by design, rather than sprinkling null checks throughout editor code.

Applied to files:

  • MCPForUnity/Editor/Windows/Components/Connection/McpConnectionSection.cs
  • MCPForUnity/Editor/Services/ServerManagementService.cs
  • MCPForUnity/Editor/Windows/Components/ClientConfig/McpClientConfigSection.cs
🧬 Code graph analysis (1)
MCPForUnity/Editor/Windows/Components/Connection/McpConnectionSection.cs (4)
MCPForUnity/Editor/Services/ServerManagementService.cs (6)
  • System (1328-1440)
  • IsLocalUrl (1290-1294)
  • IsLocalUrl (1299-1313)
  • IsLocalHttpServerRunning (641-701)
  • StopLocalHttpServer (636-639)
  • StartLocalHttpServer (421-516)
MCPForUnity/Editor/Helpers/McpLog.cs (4)
  • McpLog (7-52)
  • Info (37-41)
  • Warn (43-46)
  • Error (48-51)
MCPForUnity/Editor/Helpers/HttpEndpointUtility.cs (3)
  • HttpEndpointUtility (12-85)
  • SaveBaseUrl (29-33)
  • GetBaseUrl (20-24)
MCPForUnity/Editor/Services/Transport/Transports/WebSocketTransportClient.cs (1)
  • Uri (704-721)
🔇 Additional comments (9)
MCPForUnity/Editor/Windows/Components/ClientConfig/McpClientConfigSection.cs (2)

292-303: LGTM! Clean delegation with conditional force-immediate logic.

The parameter change to RefreshSelectedClient(bool forceImmediate = false) with the computed shouldForceImmediate provides a sensible default where non-Claude CLI clients always refresh immediately while Claude CLI clients respect the explicit parameter.


336-353: Good thread-safety pattern: capturing main-thread values before async execution.

Capturing projectDir and useHttpTransport on the main thread before Task.Run is the correct approach since Application.dataPath and EditorPrefs are not thread-safe. The defensive type check inside the task prevents crashes if the method is called with an unexpected configurator type.

MCPForUnity/Editor/Windows/Components/Connection/McpConnectionSection.cs (4)

22-27: LGTM! Clear transport protocol enum expansion.

Splitting HTTP into HTTPLocal and HTTPRemote provides explicit transport selection, which aligns with the PR objective of making the HTTP workflow more explicit.


99-140: Good back-compat inference for existing installations.

The logic correctly infers HTTPLocal vs HTTPRemote based on IsLocalUrl() when the scope preference isn't set yet, providing a smooth upgrade path for users.


184-194: Good UX improvement: persist URL on focus-out/Enter instead of every keystroke.

This prevents fighting the user during typing and avoids duplicate scheme issues from premature normalization.


424-434: Reasonable polling throttle to avoid expensive port/PID checks.

The 0.75-second throttle with an exception for in-progress toggles is a practical optimization. The cached lastLocalServerRunning state provides responsive UI while avoiding excessive system calls.

MCPForUnity/Editor/Services/ServerManagementService.cs (3)

1164-1209: Graceful process termination with escalation is well-implemented.

The pattern of sending SIGTERM, waiting up to 8 seconds, then escalating to SIGKILL is a reasonable approach. The use of ProcessExistsUnix to verify termination before escalating avoids unnecessary force-kills.

However, note that the blocking Thread.Sleep(100) loop (lines 1191-1197) can block the caller for up to 8 seconds. Since this is called from UI event handlers via OnHttpServerToggleClicked, this could cause brief UI freezes if the process is slow to terminate.


1336-1370: Good improvement: Script-based terminal launching avoids AppleScript permission prompts.

Using .command files on macOS and .cmd files on Windows is more reliable than complex AppleScript or nested quote escaping. This approach is less likely to trigger automation permission dialogs on macOS.


458-468: Handshake-based server launch with pidfile and instance token.

The per-launch instance token and pidfile provide deterministic shutdown semantics, which is a significant improvement over pure port/PID heuristics. This aligns well with the PR objective of safer lifecycle management.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
MCPForUnity/Editor/Services/ServerManagementService.cs (1)

1141-1162: Consider async alternative to Thread.Sleep in termination loop.

The graceful termination loop uses Thread.Sleep(100) which blocks the Unity main thread for up to 8 seconds. While this is bounded and only occurs during server cleanup, consider refactoring to await Task.Delay to keep the editor responsive.

🔎 Example async refactor
-                    // Wait briefly for graceful shutdown.
-                    var deadline = DateTime.UtcNow + TimeSpan.FromSeconds(8);
-                    while (DateTime.UtcNow < deadline)
-                    {
-                        if (!ProcessExistsUnix(pid))
-                        {
-                            return true;
-                        }
-                        System.Threading.Thread.Sleep(100);
-                    }
+                    // Wait briefly for graceful shutdown (async to avoid blocking Unity thread).
+                    var deadline = DateTime.UtcNow + TimeSpan.FromSeconds(8);
+                    while (DateTime.UtcNow < deadline)
+                    {
+                        if (!ProcessExistsUnix(pid))
+                        {
+                            return true;
+                        }
+                        await Task.Delay(100);
+                    }

Note: This would require changing the method signature to async Task<bool> and updating all call sites.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 85932f6 and 9b6e198.

📒 Files selected for processing (2)
  • MCPForUnity/Editor/Services/ServerManagementService.cs
  • MCPForUnity/Editor/Windows/Components/Connection/McpConnectionSection.cs
🧰 Additional context used
🧠 Learnings (8)
📚 Learning: 2025-12-29T15:23:11.613Z
Learnt from: msanatan
Repo: CoplayDev/unity-mcp PR: 491
File: MCPForUnity/Editor/Windows/EditorPrefs/EditorPrefsWindow.cs:78-115
Timestamp: 2025-12-29T15:23:11.613Z
Learning: In MCPForUnity, prefer relying on the established testing process to catch UI initialization issues instead of adding defensive null checks for UI elements in editor windows. This means during reviews, verify that tests cover UI initialization paths and that code avoids repetitive null-check boilerplate in Editor Windows. If a UI element can be null, ensure there is a well-tested fallback or that its initialization is guaranteed by design, rather than sprinkling null checks throughout editor code.

Applied to files:

  • MCPForUnity/Editor/Windows/Components/Connection/McpConnectionSection.cs
  • MCPForUnity/Editor/Services/ServerManagementService.cs
📚 Learning: 2025-09-05T16:22:04.960Z
Learnt from: dsarno
Repo: CoplayDev/unity-mcp PR: 265
File: README.md:204-204
Timestamp: 2025-09-05T16:22:04.960Z
Learning: In the Unity MCP project, the ServerInstaller.cs creates a symlink from ~/Library/AppSupport to ~/Library/Application Support on macOS to mitigate argument parsing and quoting issues in some MCP clients. The README documentation should use the shortened AppSupport path, not the full "Application Support" path with spaces.

Applied to files:

  • MCPForUnity/Editor/Services/ServerManagementService.cs
📚 Learning: 2025-09-05T16:22:04.960Z
Learnt from: dsarno
Repo: CoplayDev/unity-mcp PR: 265
File: README.md:204-204
Timestamp: 2025-09-05T16:22:04.960Z
Learning: In the Unity MCP project, the ServerInstaller.cs creates a symlink from ~/Library/AppSupport to ~/Library/Application Support on macOS to avoid spaces in paths that can cause arg parsing and quoting issues in some MCP clients. The README documentation should use the shortened AppSupport path, not the full "Application Support" path.

Applied to files:

  • MCPForUnity/Editor/Services/ServerManagementService.cs
📚 Learning: 2025-12-19T21:09:44.740Z
Learnt from: msanatan
Repo: CoplayDev/unity-mcp PR: 468
File: Server/DOCKER_OVERVIEW.md:22-22
Timestamp: 2025-12-19T21:09:44.740Z
Learning: In the Unity MCP project, the Docker workflow (.github/workflows/publish-docker.yml) intentionally uses the DOCKER_USERNAME secret instead of hardcoding "msanatan" to allow users who fork the repository to publish to their own Docker Hub repositories, while the documentation (Server/DOCKER_OVERVIEW.md) correctly references the official image location "msanatan/mcp-for-unity-server" for end users.

Applied to files:

  • MCPForUnity/Editor/Services/ServerManagementService.cs
📚 Learning: 2025-10-13T13:27:23.040Z
Learnt from: msanatan
Repo: CoplayDev/unity-mcp PR: 316
File: TestProjects/UnityMCPTests/Assets/Tests/EditMode/Resources.meta:1-8
Timestamp: 2025-10-13T13:27:23.040Z
Learning: UnityMcpBridge is a legacy project kept for backwards compatibility; MCPForUnity is the only active Unity plugin project. GUID collisions between UnityMcpBridge and MCPForUnity are acceptable.

Applied to files:

  • MCPForUnity/Editor/Services/ServerManagementService.cs
📚 Learning: 2025-11-05T18:23:12.349Z
Learnt from: msanatan
Repo: CoplayDev/unity-mcp PR: 368
File: MCPForUnity/UnityMcpServer~/src/resources/menu_items.py:15-15
Timestamp: 2025-11-05T18:23:12.349Z
Learning: In Unity MCP, the `name` parameter in the `mcp_for_unity_resource` decorator is the external API name exposed to MCP clients (LLMs, AI agents). The command string passed to `async_send_command_with_retry` or `async_send_with_unity_instance` (e.g., "get_menu_items") is the internal command identifier that must match the C# side. These are decoupled, allowing external API naming to evolve independently of internal command routing.

Applied to files:

  • MCPForUnity/Editor/Services/ServerManagementService.cs
📚 Learning: 2025-10-13T13:41:00.086Z
Learnt from: JohanHoltby
Repo: CoplayDev/unity-mcp PR: 309
File: MCPForUnity/Editor/Helpers/ServerInstaller.cs:478-508
Timestamp: 2025-10-13T13:41:00.086Z
Learning: In the MCPForUnityTools feature (MCPForUnity/Editor/Helpers/ServerInstaller.cs), the design intentionally forces users to have only one .py file per MCPForUnityTools folder to keep file tracking simple. Package-style tools (subdirectories with __init__.py) are not supported.

Applied to files:

  • MCPForUnity/Editor/Services/ServerManagementService.cs
📚 Learning: 2025-09-03T16:00:55.839Z
Learnt from: dsarno
Repo: CoplayDev/unity-mcp PR: 0
File: :0-0
Timestamp: 2025-09-03T16:00:55.839Z
Learning: ComponentResolver in UnityMcpBridge/Editor/Tools/ManageGameObject.cs is a nested static class within ManageGameObject, not a sibling type. The `using static MCPForUnity.Editor.Tools.ManageGameObject;` import is required to access ComponentResolver methods directly without the outer class qualifier.

Applied to files:

  • MCPForUnity/Editor/Services/ServerManagementService.cs
🧬 Code graph analysis (2)
MCPForUnity/Editor/Windows/Components/Connection/McpConnectionSection.cs (5)
MCPForUnity/Editor/Services/ServerManagementService.cs (4)
  • IsLocalUrl (1250-1254)
  • IsLocalUrl (1259-1273)
  • StopLocalHttpServer (589-592)
  • StartLocalHttpServer (421-516)
MCPForUnity/Editor/Services/IServerManagementService.cs (3)
  • IsLocalUrl (44-44)
  • StopLocalHttpServer (24-24)
  • StartLocalHttpServer (19-19)
MCPForUnity/Editor/Helpers/McpLog.cs (4)
  • McpLog (7-52)
  • Info (37-41)
  • Warn (43-46)
  • Error (48-51)
MCPForUnity/Editor/Helpers/HttpEndpointUtility.cs (3)
  • HttpEndpointUtility (12-85)
  • SaveBaseUrl (29-33)
  • GetBaseUrl (20-24)
MCPForUnity/Editor/Services/Transport/Transports/WebSocketTransportClient.cs (1)
  • Uri (704-721)
MCPForUnity/Editor/Services/ServerManagementService.cs (4)
MCPForUnity/Editor/Constants/EditorPrefKeys.cs (1)
  • EditorPrefKeys (7-56)
MCPForUnity/Editor/Helpers/ExecPath.cs (2)
  • ExecPath (12-272)
  • TryRun (161-227)
MCPForUnity/Editor/Helpers/HttpEndpointUtility.cs (2)
  • HttpEndpointUtility (12-85)
  • GetBaseUrl (20-24)
MCPForUnity/Editor/Helpers/McpLog.cs (5)
  • McpLog (7-52)
  • Info (37-41)
  • Warn (43-46)
  • Error (48-51)
  • Debug (31-35)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Sourcery review
🔇 Additional comments (26)
MCPForUnity/Editor/Windows/Components/Connection/McpConnectionSection.cs (12)

22-27: LGTM: Transport enum split is clean.

The HTTPLocal/HTTPRemote split provides clear semantics for the transport selection and aligns with the PR's goal of making HTTP workflows explicit.


64-65: LGTM: Event-driven architecture.

The new events enable clean coordination with the editor window and client config sections.


99-141: LGTM: Back-compat inference is well-designed.

The initialization gracefully infers HTTPLocal vs HTTPRemote from the current URL when the explicit scope preference isn't set, ensuring smooth upgrades. The health tooltip addition improves discoverability.


145-193: LGTM: Transport switching with best-effort cleanup.

The logic correctly stops the deselected transport only when switching between HTTP (local/remote) and Stdio, avoiding unnecessary stops when toggling HTTPLocal ↔ HTTPRemote. The async continuation on TaskScheduler.Default keeps the UI responsive.


197-205: LGTM: Debounced URL normalization improves UX.

Persisting and normalizing only on focus-out or Enter prevents the editor from fighting with the user during typing. Using SetValueWithoutNotify avoids recursive callbacks.

Also applies to: 250-262


207-222: LGTM: Consolidated Start/Stop button.

Routing both start and stop buttons to OnHttpServerToggleClicked consolidates the UX while maintaining backward compatibility with older UXML layouts.


264-332: LGTM: Context-aware UI visibility.

The visibility logic reduces clutter by adapting controls based on transport selection, debug mode, and health state. Hiding the manual session toggle when local server controls are active avoids confusion.


364-374: LGTM: Clear validation feedback.

Displaying a clear warning when HTTP Local is selected with a non-localhost URL prevents user confusion and misconfiguration.


435-445: LGTM: Smart polling optimization.

Throttling IsLocalHttpServerRunning() checks to once per 0.75 seconds avoids expensive port/PID queries on every UI tick while keeping the UI responsive.


471-525: LGTM: Consolidated server toggle with proper guards.

The re-entry guard and three-path logic (stop session+server, stop server only, or start server+session) correctly handle all states. The async flow keeps the UI responsive.


608-660: LGTM: Auto-start with reasonable retry policy.

The auto-start flow waits for server readiness (up to 10 seconds) and then retries session start up to 10 times with 1-second delays. This balances robustness with timely feedback.


662-717: LGTM: Readiness polling with wildcard host normalization.

The implementation correctly normalizes wildcard/bind-all hosts (0.0.0.0, ::, *, empty) to localhost before polling, addressing the prior review concern. The TCP connection with 250ms timeout is lightweight and non-blocking.

Addresses past review comment about auto-connect polling misbehaving for wildcard/bind-all HTTP hosts.

MCPForUnity/Editor/Services/ServerManagementService.cs (14)

20-47: LGTM: Project-scoped path management.

Using Library/MCPForUnity for logs, pidfiles, and scripts keeps state project-scoped and out of version control, which is the right approach for Unity projects.


49-65: LGTM: String normalization utilities.

QuoteIfNeeded and NormalizeForMatch are simple, focused helpers that improve cross-platform command construction and matching robustness.


67-120: LGTM: Clean handshake state management.

The helpers for storing/retrieving pidfile path and instance token provide a clear API for the deterministic stop flow. Swallowing exceptions on EditorPrefs operations is acceptable for resilience.


122-166: LGTM: Robust PID file handling.

The helpers construct project-scoped pidfile paths and tolerate extra whitespace/newlines when parsing, which improves robustness against manual edits or platform quirks.


168-200: LGTM: Token validation reduces PID reuse risk.

Validating the instance token in the process command line provides strong evidence that the PID still belongs to the Unity-launched server, reducing false positives from PID reuse.


202-241: LGTM: Fingerprinting with short hash.

Using a truncated SHA-256 hash as a process fingerprint provides a stable, cross-platform identifier without storing the full command line in EditorPrefs.


243-275: LGTM: 6-hour TTL limits PID reuse risk.

The 6-hour time-to-live on stored PIDs is a pragmatic balance between convenience (survives domain reload, editor restart) and safety (limits PID reuse risk).


421-516: LGTM: Deterministic server start with handshake.

The start flow creates a per-launch token and pidfile, stores the handshake state, and launches the server with augmented command-line arguments. This enables deterministic stop without relying on heuristics.


518-584: LGTM: Log viewer with session-scoped deduplication.

The session-scoped flag prevents spawning multiple terminal windows for the same log file during a single Unity session, which improves UX without requiring complex AppleScript or window tracking.


594-654: LGTM: Multi-tiered server detection.

The three-tier approach (handshake → stored PID → heuristic) balances deterministic detection with fallback robustness for edge cases like domain reload or manual server launch.


691-771: LGTM: Deterministic stop via handshake.

The handshake-based stop path (pidfile + token validation) provides deterministic termination without relying on port/process heuristics. The fallback to LooksLikeMcpServerProcess when token validation is unavailable is pragmatic.


831-882: LGTM: Fixed Unity process detection logic.

The code now correctly checks for MCP indicators (mcp-for-unity, mcp_for_unity, mcpforunity) before checking for "unity" substring, preventing false positives where "mcp-for-unity" would be mistaken for a Unity process.

Addresses past review comment about "unity" substring check matching "mcp-for-unity" and refusing to stop the server.


1191-1245: LGTM: Command construction refactored for flexibility.

Splitting the command construction into TryGetLocalHttpServerCommandParts allows the terminal launcher to use separate fileName and arguments, improving cross-platform quoting and escaping.


1057-1072: Critical: "unity" substring check still matches "mcp-for-unity".

Lines 1057-1066 correctly check for MCP indicators first, but line 1069 still checks s.Contains("unity") without excluding MCP processes. Since "mcp-for-unity" contains "unity", this will cause false positives and prevent stopping the server.

This is the same bug that was fixed in lines 835-840 but remains here.

🔎 Proposed fix

Apply the same pattern used in lines 835-840:

                 if (!string.IsNullOrEmpty(s))
                 {
 
                     bool mentionsMcp = sCompact.Contains("mcp-for-unity")
                                        || sCompact.Contains("mcp_for_unity")
                                        || sCompact.Contains("mcpforunity");
 
                     // If it explicitly mentions the server package/entrypoint, that is sufficient.
                     // Note: Check before Unity exclusion since "mcp-for-unity" contains "unity".
                     if (mentionsMcp)
                     {
                         return true;
                     }
 
                     // Explicitly never kill Unity / Unity Hub processes
-                    if (s.Contains("unityhub") || s.Contains("unity hub") || s.Contains("unity"))
+                    if (s.Contains("unityhub") || s.Contains("unity hub") || (s.Contains("unity") && !mentionsMcp))
                     {
                         return false;
                     }

This ensures MCP processes are not excluded by the Unity check.

Likely an incorrect or invalid review comment.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (5)
MCPForUnity/Editor/Services/ServerManagementService.cs (5)

22-23: Consider resetting OpenedHttpServerLogViewerThisSession on domain reload.

OpenedHttpServerLogViewerThisSession prevents duplicate log viewer windows within a session, but Unity domain reloads (entering play mode, script recompilation) may reset this field while the Terminal window remains open. This could lead to multiple Terminal windows after domain reloads.

If this is acceptable behavior (one Terminal per domain reload session), document it with a comment. Otherwise, consider persisting this flag via EditorPrefs with a session ID to survive domain reloads.


49-53: QuoteIfNeeded does not escape existing quotes in path.

If s contains double quotes, the result will be malformed (e.g., "/some"path" becomes ""/some"path""). While Unity project paths rarely contain quotes, consider escaping or warning when this edge case occurs.

🔎 Proposed enhancement
 private static string QuoteIfNeeded(string s)
 {
     if (string.IsNullOrEmpty(s)) return s;
+    if (s.Contains("\""))
+    {
+        // Edge case: path contains quotes. Escape for shell safety.
+        s = s.Replace("\"", "\\\"");
+    }
     return s.IndexOf(' ') >= 0 ? $"\"{s}\"" : s;
 }

498-502: Consider storing the launched process in LocalHttpServerProcess for tracking.

Line 500 starts the server process but doesn't capture the returned Process object. The StopLocalHttpServerInternal method (lines 775-797) checks LocalHttpServerProcess and attempts to terminate it first if available.

Storing the launched process would enable more reliable cleanup:

-                    var startInfo = CreateTerminalProcessStartInfo(launchCommand);
-                    System.Diagnostics.Process.Start(startInfo);
+                    var startInfo = CreateTerminalProcessStartInfo(launchCommand);
+                    lock (LocalHttpServerProcessLock)
+                    {
+                        LocalHttpServerProcess = System.Diagnostics.Process.Start(startInfo);
+                    }
                     McpLog.Info($"Started local HTTP server in terminal: {launchCommand}");

However, note that on macOS and Windows, the terminal launcher scripts create intermediate processes, so the returned Process object may not be the actual server process. Consider documenting this limitation.


775-797: Ensure LocalHttpServerProcess is always disposed even if termination fails.

The finally block (lines 792-796) disposes LocalHttpServerProcess, which is correct. However, if TerminateProcess throws an exception (line 784), the process handle might leak.

Consider wrapping TerminateProcess in try-catch or ensuring all paths dispose the handle:

                     try
                     {
                         if (LocalHttpServerProcess != null && !LocalHttpServerProcess.HasExited)
                         {
                             int pidToKill = LocalHttpServerProcess.Id;
                             if (unityPid <= 0 || pidToKill != unityPid)
                             {
-                                if (TerminateProcess(pidToKill))
+                                try
+                                {
+                                    if (TerminateProcess(pidToKill))
+                                    {
+                                        stoppedAny = true;
+                                    }
+                                }
+                                catch (Exception ex)
                                 {
-                                    stoppedAny = true;
+                                    McpLog.Warn($"Exception terminating tracked process: {ex.Message}");
                                 }
                             }
                         }
                     }
-                    catch { }
+                    catch (Exception ex)
+                    {
+                        McpLog.Debug($"Error accessing LocalHttpServerProcess: {ex.Message}");
+                    }
                     finally
                     {
                         try { LocalHttpServerProcess?.Dispose(); } catch { }
                         LocalHttpServerProcess = null;
                     }

1125-1170: Blocking Thread.Sleep in Unix termination may freeze Unity Editor briefly.

Lines 1150-1158 use Thread.Sleep(100) in a loop for up to 8 seconds, blocking the Unity main thread. This may cause editor lag during server shutdown.

For typical use cases (shutdown during editor quit or transport switch), brief blocking is acceptable. However, consider warning users if shutdown takes > 2 seconds:

                     var deadline = DateTime.UtcNow + TimeSpan.FromSeconds(8);
+                    var warnedAt = DateTime.UtcNow + TimeSpan.FromSeconds(2);
+                    bool warned = false;
                     while (DateTime.UtcNow < deadline)
                     {
                         if (!ProcessExistsUnix(pid))
                         {
                             return true;
                         }
+                        if (!warned && DateTime.UtcNow >= warnedAt)
+                        {
+                            McpLog.Info($"Waiting for server process {pid} to exit gracefully...");
+                            warned = true;
+                        }
                         System.Threading.Thread.Sleep(100);
                     }
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9b6e198 and 3d3fa8e.

📒 Files selected for processing (1)
  • MCPForUnity/Editor/Services/ServerManagementService.cs
🧰 Additional context used
🧠 Learnings (7)
📚 Learning: 2025-09-05T16:22:04.960Z
Learnt from: dsarno
Repo: CoplayDev/unity-mcp PR: 265
File: README.md:204-204
Timestamp: 2025-09-05T16:22:04.960Z
Learning: In the Unity MCP project, the ServerInstaller.cs creates a symlink from ~/Library/AppSupport to ~/Library/Application Support on macOS to mitigate argument parsing and quoting issues in some MCP clients. The README documentation should use the shortened AppSupport path, not the full "Application Support" path with spaces.

Applied to files:

  • MCPForUnity/Editor/Services/ServerManagementService.cs
📚 Learning: 2025-09-05T16:22:04.960Z
Learnt from: dsarno
Repo: CoplayDev/unity-mcp PR: 265
File: README.md:204-204
Timestamp: 2025-09-05T16:22:04.960Z
Learning: In the Unity MCP project, the ServerInstaller.cs creates a symlink from ~/Library/AppSupport to ~/Library/Application Support on macOS to avoid spaces in paths that can cause arg parsing and quoting issues in some MCP clients. The README documentation should use the shortened AppSupport path, not the full "Application Support" path.

Applied to files:

  • MCPForUnity/Editor/Services/ServerManagementService.cs
📚 Learning: 2025-12-19T21:09:44.740Z
Learnt from: msanatan
Repo: CoplayDev/unity-mcp PR: 468
File: Server/DOCKER_OVERVIEW.md:22-22
Timestamp: 2025-12-19T21:09:44.740Z
Learning: In the Unity MCP project, the Docker workflow (.github/workflows/publish-docker.yml) intentionally uses the DOCKER_USERNAME secret instead of hardcoding "msanatan" to allow users who fork the repository to publish to their own Docker Hub repositories, while the documentation (Server/DOCKER_OVERVIEW.md) correctly references the official image location "msanatan/mcp-for-unity-server" for end users.

Applied to files:

  • MCPForUnity/Editor/Services/ServerManagementService.cs
📚 Learning: 2025-12-29T15:23:11.613Z
Learnt from: msanatan
Repo: CoplayDev/unity-mcp PR: 491
File: MCPForUnity/Editor/Windows/EditorPrefs/EditorPrefsWindow.cs:78-115
Timestamp: 2025-12-29T15:23:11.613Z
Learning: In MCPForUnity, prefer relying on the established testing process to catch UI initialization issues instead of adding defensive null checks for UI elements in editor windows. This means during reviews, verify that tests cover UI initialization paths and that code avoids repetitive null-check boilerplate in Editor Windows. If a UI element can be null, ensure there is a well-tested fallback or that its initialization is guaranteed by design, rather than sprinkling null checks throughout editor code.

Applied to files:

  • MCPForUnity/Editor/Services/ServerManagementService.cs
📚 Learning: 2025-11-05T18:23:12.349Z
Learnt from: msanatan
Repo: CoplayDev/unity-mcp PR: 368
File: MCPForUnity/UnityMcpServer~/src/resources/menu_items.py:15-15
Timestamp: 2025-11-05T18:23:12.349Z
Learning: In Unity MCP, the `name` parameter in the `mcp_for_unity_resource` decorator is the external API name exposed to MCP clients (LLMs, AI agents). The command string passed to `async_send_command_with_retry` or `async_send_with_unity_instance` (e.g., "get_menu_items") is the internal command identifier that must match the C# side. These are decoupled, allowing external API naming to evolve independently of internal command routing.

Applied to files:

  • MCPForUnity/Editor/Services/ServerManagementService.cs
📚 Learning: 2025-10-13T13:27:23.040Z
Learnt from: msanatan
Repo: CoplayDev/unity-mcp PR: 316
File: TestProjects/UnityMCPTests/Assets/Tests/EditMode/Resources.meta:1-8
Timestamp: 2025-10-13T13:27:23.040Z
Learning: UnityMcpBridge is a legacy project kept for backwards compatibility; MCPForUnity is the only active Unity plugin project. GUID collisions between UnityMcpBridge and MCPForUnity are acceptable.

Applied to files:

  • MCPForUnity/Editor/Services/ServerManagementService.cs
📚 Learning: 2025-09-03T16:00:55.839Z
Learnt from: dsarno
Repo: CoplayDev/unity-mcp PR: 0
File: :0-0
Timestamp: 2025-09-03T16:00:55.839Z
Learning: ComponentResolver in UnityMcpBridge/Editor/Tools/ManageGameObject.cs is a nested static class within ManageGameObject, not a sibling type. The `using static MCPForUnity.Editor.Tools.ManageGameObject;` import is required to access ComponentResolver methods directly without the outer class qualifier.

Applied to files:

  • MCPForUnity/Editor/Services/ServerManagementService.cs
🧬 Code graph analysis (1)
MCPForUnity/Editor/Services/ServerManagementService.cs (4)
MCPForUnity/Editor/Constants/EditorPrefKeys.cs (1)
  • EditorPrefKeys (7-56)
MCPForUnity/Editor/Helpers/ExecPath.cs (2)
  • ExecPath (12-272)
  • TryRun (161-227)
MCPForUnity/Editor/Services/Transport/Transports/WebSocketTransportClient.cs (1)
  • Uri (704-721)
MCPForUnity/Editor/Helpers/McpLog.cs (5)
  • McpLog (7-52)
  • Info (37-41)
  • Warn (43-46)
  • Error (48-51)
  • Debug (31-35)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Sourcery review
🔇 Additional comments (9)
MCPForUnity/Editor/Services/ServerManagementService.cs (9)

168-200: LGTM! Token validation reduces PID reuse risk.

The per-launch token validation is a solid approach to avoid terminating wrong processes after PID reuse. The fallback to LooksLikeMcpServerProcess when token validation is unavailable (line 740) is appropriate.


202-275: LGTM! Stored PID validation with age limit is well-designed.

The 6-hour age limit (line 262) is a pragmatic balance between avoiding PID reuse false positives and surviving editor restarts/domain reloads. The fingerprint hash (lines 221-241) provides additional validation.


518-584: LGTM! Log viewer approach avoids AppleScript permission issues.

The .command script approach on macOS (lines 542-563) is a good workaround for automation permission prompts. The note about expected transient errors (lines 528-530) will help users interpret logs.


594-654: LGTM! Best-effort server status check is appropriate for UI state.

The tiered approach (handshake → stored PID → heuristics) matches the stop logic and provides a reasonable confidence level for UI state display.


938-962: LGTM! Unix process args fetching is robust.

The -ww flag (line 951) prevents truncation of long command lines, which is essential for reliably detecting "mcp-for-unity" in process arguments. The normalization (line 955) enables consistent matching.


1027-1101: LGTM! Process fingerprinting correctly handles "mcp-for-unity" vs "unity" ambiguity.

Lines 1057-1066 check for MCP indicators first, then lines 1069-1073 exclude Unity processes only if MCP indicators are absent. This correctly handles the case where "mcp-for-unity" contains "unity". Per past review comments, this bug was addressed.


1206-1246: LGTM! Command building is clear and handles dev mode correctly.

The split between fileName/arguments (for ProcessStartInfo) and displayCommand (for user copy-paste) is clean. Dev mode flags at line 1237 align with the comment at line 456 about cache-busting.


1289-1401: LGTM! Script-based terminal launching avoids brittle quoting issues.

Writing scripts to Library/MCPForUnity/TerminalScripts (lines 1298, 1317) is a robust approach that sidesteps shell quoting complexity. The macOS implementation (lines 1296-1314) avoids AppleScript automation permission prompts, as noted in the comment.


417-516: Verify handshake-based lifecycle in manual testing scenarios.

The handshake-based lifecycle (pidfile + token) is a significant improvement for deterministic stop behavior. Ensure manual testing covers:

  1. Happy path: Start → Stop within same editor session
  2. Domain reload: Start → Domain reload (enter play mode) → Stop (should still work via pidfile)
  3. Editor restart: Start → Quit editor → Reopen → Stop (should detect stale pidfile and refuse stop if port reused)
  4. Manual server launch: Launch via terminal manually (no handshake) → Stop via UI (should fall back to heuristics)
  5. Port conflict: Start while another process owns the port (should refuse and show clear error)

Per the PR summary, "pytest OK (83 passed, 2 skipped, 7 xpassed); manual Unity verification of pidfile creation and deterministic stop" suggests this is covered. Consider documenting these scenarios in a test plan.

Also applies to: 586-936

Comment on lines +128 to +133
private static string GetLocalHttpServerPidFilePath(int port)
{
string dir = GetLocalHttpServerPidDirectory();
Directory.CreateDirectory(dir);
return Path.Combine(dir, $"mcp_http_{port}.pid");
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Directory.CreateDirectory may throw if permissions are denied.

If the Library/MCPForUnity/RunState directory cannot be created due to permission issues, this will throw an exception. The caller (StartLocalHttpServer) should handle this gracefully.

Consider wrapping in try-catch and returning a default path or logging an error.

🔎 Proposed fix
 private static string GetLocalHttpServerPidFilePath(int port)
 {
-    string dir = GetLocalHttpServerPidDirectory();
-    Directory.CreateDirectory(dir);
-    return Path.Combine(dir, $"mcp_http_{port}.pid");
+    try
+    {
+        string dir = GetLocalHttpServerPidDirectory();
+        Directory.CreateDirectory(dir);
+        return Path.Combine(dir, $"mcp_http_{port}.pid");
+    }
+    catch (Exception ex)
+    {
+        McpLog.Warn($"Failed to create pidfile directory: {ex.Message}");
+        return null;
+    }
 }

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
MCPForUnity/Editor/Services/ServerManagementService.cs around lines 128-133:
Directory.CreateDirectory can throw (e.g.
UnauthorizedAccessException/IOException) when permissions are denied; wrap the
directory creation in a try-catch, catch relevant exceptions, log the error
(using Debug.LogError or the service logger) and return a safe fallback PID path
(for example in Path.GetTempPath() or a known writable location) so
StartLocalHttpServer can continue or handle the failure gracefully instead of
letting an exception propagate.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
MCPForUnity/Editor/Services/ServerManagementService.cs (1)

114-119: Directory.CreateDirectory may throw if permissions are denied.

This was flagged in a previous review. The caller (StartLocalHttpServer) should handle this gracefully, or wrap the directory creation in try-catch here.

🧹 Nitpick comments (2)
MCPForUnity/Editor/Services/ServerManagementService.cs (2)

20-20: Static HashSet grows unbounded during long editor sessions.

LoggedStopDiagnosticsPids is never cleared, so PIDs accumulate across stop attempts. While unlikely to cause memory issues in practice, consider clearing it periodically (e.g., in ClearLocalServerPidTracking) or using a bounded cache.

🔎 Proposed fix
 private static void ClearLocalServerPidTracking()
 {
+    try { LoggedStopDiagnosticsPids.Clear(); } catch { }
     try { EditorPrefs.DeleteKey(EditorPrefKeys.LastLocalHttpServerPid); } catch { }
     // ... rest of method
 }

177-180: Minor: Redundant lowercasing of tokenNeedle.

tokenNeedle is already lowercased (line 164), and NormalizeForMatch also lowercases. This is harmless but slightly redundant.

🔎 Proposed fix
                 if (TryGetUnixProcessArgs(pid, out var argsLowerNow))
                 {
-                    containsToken = argsLowerNow.Contains(NormalizeForMatch(tokenNeedle));
+                    containsToken = argsLowerNow.Contains(tokenNeedle); // argsLowerNow is already normalized
                     return true;
                 }
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3d3fa8e and 8dae8e2.

📒 Files selected for processing (5)
  • MCPForUnity/Editor/Services/IServerManagementService.cs
  • MCPForUnity/Editor/Services/McpEditorShutdownCleanup.cs
  • MCPForUnity/Editor/Services/ServerManagementService.cs
  • MCPForUnity/Editor/Windows/Components/Connection/McpConnectionSection.cs
  • Server/src/services/tools/read_console.py
🧰 Additional context used
🧠 Learnings (7)
📚 Learning: 2025-12-29T15:23:11.613Z
Learnt from: msanatan
Repo: CoplayDev/unity-mcp PR: 491
File: MCPForUnity/Editor/Windows/EditorPrefs/EditorPrefsWindow.cs:78-115
Timestamp: 2025-12-29T15:23:11.613Z
Learning: In MCPForUnity, prefer relying on the established testing process to catch UI initialization issues instead of adding defensive null checks for UI elements in editor windows. This means during reviews, verify that tests cover UI initialization paths and that code avoids repetitive null-check boilerplate in Editor Windows. If a UI element can be null, ensure there is a well-tested fallback or that its initialization is guaranteed by design, rather than sprinkling null checks throughout editor code.

Applied to files:

  • MCPForUnity/Editor/Services/IServerManagementService.cs
  • MCPForUnity/Editor/Services/ServerManagementService.cs
  • MCPForUnity/Editor/Windows/Components/Connection/McpConnectionSection.cs
  • MCPForUnity/Editor/Services/McpEditorShutdownCleanup.cs
📚 Learning: 2025-09-05T16:22:04.960Z
Learnt from: dsarno
Repo: CoplayDev/unity-mcp PR: 265
File: README.md:204-204
Timestamp: 2025-09-05T16:22:04.960Z
Learning: In the Unity MCP project, the ServerInstaller.cs creates a symlink from ~/Library/AppSupport to ~/Library/Application Support on macOS to mitigate argument parsing and quoting issues in some MCP clients. The README documentation should use the shortened AppSupport path, not the full "Application Support" path with spaces.

Applied to files:

  • MCPForUnity/Editor/Services/ServerManagementService.cs
📚 Learning: 2025-09-05T16:22:04.960Z
Learnt from: dsarno
Repo: CoplayDev/unity-mcp PR: 265
File: README.md:204-204
Timestamp: 2025-09-05T16:22:04.960Z
Learning: In the Unity MCP project, the ServerInstaller.cs creates a symlink from ~/Library/AppSupport to ~/Library/Application Support on macOS to avoid spaces in paths that can cause arg parsing and quoting issues in some MCP clients. The README documentation should use the shortened AppSupport path, not the full "Application Support" path.

Applied to files:

  • MCPForUnity/Editor/Services/ServerManagementService.cs
📚 Learning: 2025-12-19T21:09:44.740Z
Learnt from: msanatan
Repo: CoplayDev/unity-mcp PR: 468
File: Server/DOCKER_OVERVIEW.md:22-22
Timestamp: 2025-12-19T21:09:44.740Z
Learning: In the Unity MCP project, the Docker workflow (.github/workflows/publish-docker.yml) intentionally uses the DOCKER_USERNAME secret instead of hardcoding "msanatan" to allow users who fork the repository to publish to their own Docker Hub repositories, while the documentation (Server/DOCKER_OVERVIEW.md) correctly references the official image location "msanatan/mcp-for-unity-server" for end users.

Applied to files:

  • MCPForUnity/Editor/Services/ServerManagementService.cs
📚 Learning: 2025-11-05T18:23:12.349Z
Learnt from: msanatan
Repo: CoplayDev/unity-mcp PR: 368
File: MCPForUnity/UnityMcpServer~/src/resources/menu_items.py:15-15
Timestamp: 2025-11-05T18:23:12.349Z
Learning: In Unity MCP, the `name` parameter in the `mcp_for_unity_resource` decorator is the external API name exposed to MCP clients (LLMs, AI agents). The command string passed to `async_send_command_with_retry` or `async_send_with_unity_instance` (e.g., "get_menu_items") is the internal command identifier that must match the C# side. These are decoupled, allowing external API naming to evolve independently of internal command routing.

Applied to files:

  • MCPForUnity/Editor/Services/ServerManagementService.cs
📚 Learning: 2025-10-13T13:27:23.040Z
Learnt from: msanatan
Repo: CoplayDev/unity-mcp PR: 316
File: TestProjects/UnityMCPTests/Assets/Tests/EditMode/Resources.meta:1-8
Timestamp: 2025-10-13T13:27:23.040Z
Learning: UnityMcpBridge is a legacy project kept for backwards compatibility; MCPForUnity is the only active Unity plugin project. GUID collisions between UnityMcpBridge and MCPForUnity are acceptable.

Applied to files:

  • MCPForUnity/Editor/Services/ServerManagementService.cs
📚 Learning: 2025-09-03T16:00:55.839Z
Learnt from: dsarno
Repo: CoplayDev/unity-mcp PR: 0
File: :0-0
Timestamp: 2025-09-03T16:00:55.839Z
Learning: ComponentResolver in UnityMcpBridge/Editor/Tools/ManageGameObject.cs is a nested static class within ManageGameObject, not a sibling type. The `using static MCPForUnity.Editor.Tools.ManageGameObject;` import is required to access ComponentResolver methods directly without the outer class qualifier.

Applied to files:

  • MCPForUnity/Editor/Services/ServerManagementService.cs
🧬 Code graph analysis (3)
MCPForUnity/Editor/Services/IServerManagementService.cs (1)
MCPForUnity/Editor/Services/ServerManagementService.cs (2)
  • StopManagedLocalHttpServer (511-536)
  • IsLocalHttpServerRunning (538-598)
Server/src/services/tools/read_console.py (1)
Server/src/services/tools/utils.py (1)
  • coerce_int (63-77)
MCPForUnity/Editor/Windows/Components/Connection/McpConnectionSection.cs (5)
MCPForUnity/Editor/Constants/EditorPrefKeys.cs (1)
  • EditorPrefKeys (7-56)
MCPForUnity/Editor/Services/MCPServiceLocator.cs (1)
  • MCPServiceLocator (11-91)
MCPForUnity/Editor/Services/IServerManagementService.cs (1)
  • IsLocalUrl (50-50)
MCPForUnity/Editor/Helpers/HttpEndpointUtility.cs (3)
  • HttpEndpointUtility (12-85)
  • SaveBaseUrl (29-33)
  • GetBaseUrl (20-24)
MCPForUnity/Editor/Services/Transport/Transports/WebSocketTransportClient.cs (1)
  • Uri (704-721)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Sourcery review
🔇 Additional comments (17)
MCPForUnity/Editor/Services/ServerManagementService.cs (5)

480-486: Handshake state stored before server confirms startup.

StoreLocalHttpServerHandshake is called immediately after Process.Start, but the server may not have written its PID to the pidfile yet. This is acceptable given the polling in TryAutoStartSessionAfterHttpServerAsync, but be aware that IsLocalHttpServerRunning may return false briefly after start.


769-774: Good fix for the "unity" substring false positive.

The logic now correctly detects MCP server indicators (mcp-for-unity, mcp_for_unity, mcpforunity) before checking for Unity processes, preventing false refusals when the command line contains "mcp-for-unity".


1116-1144: Good graceful termination pattern with escalation.

The Unix termination logic follows best practices: sends SIGTERM first, waits up to 8 seconds for graceful exit, then escalates to SIGKILL only if necessary. The blocking Thread.Sleep is acceptable in this synchronous termination context.


1270-1288: Script-based terminal launch avoids macOS automation permission prompts.

Good approach: writing a .command script and opening it with Terminal avoids AppleScript permission issues. The script persists in Library/MCPForUnity/TerminalScripts but is overwritten on subsequent launches, which is acceptable.


1289-1305: Script-based Windows terminal launch avoids quote escaping issues.

Writing a .cmd script and launching it with cmd.exe /c start is a robust approach that avoids brittle nested-quote escaping in the command line.

MCPForUnity/Editor/Services/IServerManagementService.cs (1)

26-37: Interface additions are well-documented and align with implementation.

The new StopManagedLocalHttpServer() and IsLocalHttpServerRunning() methods provide clear semantics for lifecycle management. Documentation accurately describes the intent: managed stop works regardless of transport selection, and running detection is best-effort.

MCPForUnity/Editor/Services/McpEditorShutdownCleanup.cs (3)

18-23: Good defensive pattern for domain reload safety.

Unsubscribing before subscribing in the static constructor prevents duplicate event handlers across Unity domain reloads. The try-catch around unsubscribe is appropriate since the event may not have a prior subscription.


43-68: Robust cleanup logic with appropriate fallbacks.

The two-tier approach works well:

  1. If HTTP Local is selected, attempt StopLocalHttpServer() (guarded by PID heuristics)
  2. Always fall back to StopManagedLocalHttpServer() to catch orphaned Unity-managed servers

This covers scenarios like transport switching or URL changes.


32-35: Bounded wait prevents blocking Unity quit.

Using Task.WaitAll with a 750ms timeout ensures transport cleanup doesn't hang the editor quit indefinitely. The inner try-catch correctly suppresses exceptions for best-effort semantics.

MCPForUnity/Editor/Windows/Components/Connection/McpConnectionSection.cs (8)

22-27: Transport enum updated for explicit local/remote distinction.

The TransportProtocol enum now distinguishes HTTPLocal from HTTPRemote, enabling clearer UI semantics and transport-specific behavior (local server controls vs. remote URL entry).


165-192: Best-effort stop of deselected transport prevents duplicated sessions.

Good UX improvement: when switching between HTTP and Stdio, the previously active transport is stopped asynchronously. The fire-and-forget pattern with error logging via ContinueWith is appropriate here since we don't want to block the UI callback.


195-205: URL normalization on commit prevents fighting user input.

Deferring URL normalization to focus-out or Enter key prevents the jarring experience of the URL field being rewritten mid-typing. This is a significant UX improvement.


435-445: Throttled server-running checks avoid expensive polling.

The 0.75-second throttle for IsLocalHttpServerRunning() checks is a good balance between UI responsiveness and avoiding expensive port/PID queries on every repaint.


471-525: Consolidated Start/Stop toggle handles all server states correctly.

The logic correctly handles three states:

  1. Session active → end session, then stop server
  2. Server running without session → stop server directly
  3. Neither → start server and auto-start session

The re-entry guard (httpServerToggleInProgress) prevents race conditions from rapid clicks.


608-660: Auto-start session with readiness check and retries.

Good implementation:

  1. Waits up to 10 seconds for server to accept TCP connections
  2. Then retries session start up to 10 times with 1-second delays

This covers the startup race condition where the session start might fail if attempted before the server is fully ready.


675-688: Wildcard host normalization addresses past review feedback.

The code now correctly normalizes bind-all hosts (0.0.0.0, ::, *) to localhost before TCP connection checks. This fixes the issue where TcpClient.Connect would fail for wildcard hosts even though the server is listening on loopback.


693-727: TCP readiness polling with proper async disposal.

The polling loop correctly:

  • Uses Task.WhenAny with a 250ms timeout per attempt
  • Observes faulted tasks to prevent UnobservedTaskException
  • Disposes TcpClient via using statement

Note: Disposing a TcpClient while ConnectAsync is pending may throw on some platforms, but the outer catch block handles this gracefully.

Comment on lines +48 to +59
# Coerce count defensively (string/float -> int).
# Important: leaving count unset previously meant "return all console entries", which can be extremely slow
# (and can exceed the plugin command timeout when Unity has a large console).
# To keep the tool responsive by default, we cap the default to a reasonable number of most-recent entries.
# If a client truly wants everything, it can pass count="all" (or count="*") explicitly.
if isinstance(count, str) and count.strip().lower() in ("all", "*"):
count = None
else:
count = coerce_int(count)

if action == "get" and count is None:
count = 200
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Fix the logic bug: explicit "all"/"*" requests are incorrectly capped at 200.

The current implementation has a critical flaw. When a user explicitly passes count="all" or count="*", the code sets count = None (line 54), but then lines 58-59 immediately override it back to 200 for action == "get". This defeats the purpose of the "all"/"*" feature described in the comments.

Trace:

  • User passes count="all" → line 54 sets count = None
  • Line 58 condition action == "get" and count is None evaluates to True
  • Line 59 sets count = 200
  • Result: User gets 200 entries instead of all entries

Fix: Track whether the user explicitly requested all entries, and skip the default in that case.

🔎 Proposed fix
-    # Coerce count defensively (string/float -> int).
-    # Important: leaving count unset previously meant "return all console entries", which can be extremely slow
-    # (and can exceed the plugin command timeout when Unity has a large console).
-    # To keep the tool responsive by default, we cap the default to a reasonable number of most-recent entries.
-    # If a client truly wants everything, it can pass count="all" (or count="*") explicitly.
-    if isinstance(count, str) and count.strip().lower() in ("all", "*"):
-        count = None
-    else:
-        count = coerce_int(count)
-
-    if action == "get" and count is None:
-        count = 200
+    # Coerce count defensively (string/float -> int).
+    # Important: leaving count unset previously meant "return all console entries", which can be extremely slow
+    # (and can exceed the plugin command timeout when Unity has a large console).
+    # To keep the tool responsive by default, we cap the default to a reasonable number of most-recent entries.
+    # If a client truly wants everything, it can pass count="all" (or count="*") explicitly.
+    request_all_entries = isinstance(count, str) and count.strip().lower() in ("all", "*")
+    
+    if request_all_entries:
+        count = None
+    else:
+        count = coerce_int(count)
+        if action == "get" and count is None:
+            count = 200
🤖 Prompt for AI Agents
In Server/src/services/tools/read_console.py around lines 48 to 59, the logic
that handles count incorrectly overrides an explicit "all" or "*" request with
the 200 default; change it to record when the user explicitly requested all
(e.g., set a boolean explicit_all = True when count is a string and in
("all","*")), otherwise coerce count to int as before, and then only apply the
default cap (if action == "get") when explicit_all is False and count is None
(leaving count None to mean "return all" when explicit_all is True).

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (3)
MCPForUnity/Editor/Services/Transport/TransportCommandDispatcher.cs (1)

153-165: Consider defensive handling when context is unavailable.

If _mainThreadContext is null (e.g., during domain reload edge cases) and the caller is on a background thread, Invoke() executes on the background thread instead of the main thread—potentially accessing Unity APIs unsafely.

Consider adding a fallback or logging when context is unavailable:

🔎 Suggested defensive check
+            if (_mainThreadContext == null)
+            {
+                // Context unavailable (domain reload edge case); caller should retry or handle gracefully.
+                McpLog.Warn("[TransportCommandDispatcher] Main thread context unavailable; cannot guarantee main-thread execution.");
+            }
+
             if (_mainThreadContext != null && Thread.CurrentThread.ManagedThreadId != _mainThreadId)
             {
                 _mainThreadContext.Post(_ => Invoke(), null);
                 return tcs.Task;
             }
Server/src/transport/plugin_hub.py (2)

48-71: Move import json to the top of the file.

The json module is imported twice inside the function (lines 63 and 67). While Python caches module imports, this pattern is unconventional and slightly inefficient. Consider moving the import to the module level with other imports.

🔎 Proposed fix

Add at the top of the file with other imports:

import json

Then update the function:

 def _read_unity_status_file(target_hash: str | None) -> dict | None:
     """Best-effort read of the Unity status JSON written under ~/.unity-mcp/."""
     try:
         base_path = Path.home().joinpath(".unity-mcp")
         status_files = sorted(
             base_path.glob("unity-mcp-status-*.json"),
             key=lambda p: p.stat().st_mtime,
             reverse=True,
         )
         if not status_files:
             return None
         if target_hash:
             for status_path in status_files:
                 if status_path.stem.endswith(target_hash):
                     with status_path.open("r") as f:
-                        import json
-
                         return json.load(f)
         with status_files[0].open("r") as f:
-            import json
-
             return json.load(f)
     except Exception:
         return None

84-89: Consider annotating mutable class attributes with ClassVar.

Per static analysis (RUF012), mutable class attributes that are shared across instances should be annotated with typing.ClassVar to clarify they are not instance attributes.

🔎 Proposed fix

Add to imports:

from typing import Any, ClassVar

Then update the attributes:

-    _FAST_FAIL_COMMANDS: set[str] = {"read_console", "get_editor_state", "ping"}
+    _FAST_FAIL_COMMANDS: ClassVar[set[str]] = {"read_console", "get_editor_state", "ping"}

     _registry: PluginRegistry | None = None
-    _connections: dict[str, WebSocket] = {}
+    _connections: ClassVar[dict[str, WebSocket]] = {}
     # command_id -> {"future": Future, "session_id": str}
-    _pending: dict[str, dict[str, Any]] = {}
+    _pending: ClassVar[dict[str, dict[str, Any]]] = {}
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8dae8e2 and 02902f7.

📒 Files selected for processing (7)
  • MCPForUnity/Editor/Services/Transport/TransportCommandDispatcher.cs
  • MCPForUnity/Editor/Services/Transport/Transports/WebSocketTransportClient.cs
  • MCPForUnity/Editor/Tools/ReadConsole.cs
  • MCPForUnity/Editor/Windows/Components/Connection/McpConnectionSection.cs
  • Server/src/services/resources/editor_state.py
  • Server/src/transport/plugin_hub.py
  • Server/src/transport/unity_transport.py
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2025-11-05T18:23:12.349Z
Learnt from: msanatan
Repo: CoplayDev/unity-mcp PR: 368
File: MCPForUnity/UnityMcpServer~/src/resources/menu_items.py:15-15
Timestamp: 2025-11-05T18:23:12.349Z
Learning: In Unity MCP, the `name` parameter in the `mcp_for_unity_resource` decorator is the external API name exposed to MCP clients (LLMs, AI agents). The command string passed to `async_send_command_with_retry` or `async_send_with_unity_instance` (e.g., "get_menu_items") is the internal command identifier that must match the C# side. These are decoupled, allowing external API naming to evolve independently of internal command routing.

Applied to files:

  • MCPForUnity/Editor/Services/Transport/TransportCommandDispatcher.cs
  • Server/src/transport/unity_transport.py
  • Server/src/transport/plugin_hub.py
📚 Learning: 2025-12-29T15:23:11.613Z
Learnt from: msanatan
Repo: CoplayDev/unity-mcp PR: 491
File: MCPForUnity/Editor/Windows/EditorPrefs/EditorPrefsWindow.cs:78-115
Timestamp: 2025-12-29T15:23:11.613Z
Learning: In MCPForUnity, prefer relying on the established testing process to catch UI initialization issues instead of adding defensive null checks for UI elements in editor windows. This means during reviews, verify that tests cover UI initialization paths and that code avoids repetitive null-check boilerplate in Editor Windows. If a UI element can be null, ensure there is a well-tested fallback or that its initialization is guaranteed by design, rather than sprinkling null checks throughout editor code.

Applied to files:

  • MCPForUnity/Editor/Services/Transport/TransportCommandDispatcher.cs
  • MCPForUnity/Editor/Tools/ReadConsole.cs
  • MCPForUnity/Editor/Services/Transport/Transports/WebSocketTransportClient.cs
  • MCPForUnity/Editor/Windows/Components/Connection/McpConnectionSection.cs
📚 Learning: 2025-10-13T13:27:23.040Z
Learnt from: msanatan
Repo: CoplayDev/unity-mcp PR: 316
File: TestProjects/UnityMCPTests/Assets/Tests/EditMode/Resources.meta:1-8
Timestamp: 2025-10-13T13:27:23.040Z
Learning: UnityMcpBridge is a legacy project kept for backwards compatibility; MCPForUnity is the only active Unity plugin project. GUID collisions between UnityMcpBridge and MCPForUnity are acceptable.

Applied to files:

  • Server/src/transport/unity_transport.py
  • Server/src/transport/plugin_hub.py
🧬 Code graph analysis (5)
MCPForUnity/Editor/Services/Transport/TransportCommandDispatcher.cs (1)
MCPForUnity/Editor/Services/Transport/Transports/WebSocketTransportClient.cs (16)
  • Task (69-74)
  • Task (76-99)
  • Task (101-138)
  • Task (140-164)
  • Task (189-224)
  • Task (231-269)
  • Task (282-312)
  • Task (314-359)
  • Task (361-394)
  • Task (414-426)
  • Task (428-476)
  • Task (478-543)
  • Task (545-569)
  • Task (571-583)
  • Task (585-592)
  • Task (594-619)
MCPForUnity/Editor/Tools/ReadConsole.cs (1)
MCPForUnity/Editor/Helpers/Response.cs (2)
  • ErrorResponse (35-67)
  • ErrorResponse (61-66)
Server/src/transport/unity_transport.py (2)
Server/src/transport/plugin_hub.py (1)
  • send_command_for_instance (447-499)
Server/tests/integration/test_helpers.py (1)
  • model_dump (10-13)
MCPForUnity/Editor/Windows/Components/Connection/McpConnectionSection.cs (4)
MCPForUnity/Editor/Services/ServerManagementService.cs (6)
  • System (1263-1375)
  • IsLocalUrl (1225-1229)
  • IsLocalUrl (1234-1248)
  • IsLocalHttpServerRunning (538-598)
  • StopLocalHttpServer (506-509)
  • StartLocalHttpServer (407-501)
MCPForUnity/Editor/Constants/EditorPrefKeys.cs (1)
  • EditorPrefKeys (7-56)
MCPForUnity/Editor/Services/IServerManagementService.cs (4)
  • IsLocalUrl (50-50)
  • IsLocalHttpServerRunning (36-36)
  • StopLocalHttpServer (24-24)
  • StartLocalHttpServer (19-19)
MCPForUnity/Editor/Helpers/HttpEndpointUtility.cs (3)
  • HttpEndpointUtility (12-85)
  • SaveBaseUrl (29-33)
  • GetBaseUrl (20-24)
Server/src/transport/plugin_hub.py (1)
Server/tests/integration/test_helpers.py (2)
  • model_dump (10-13)
  • error (46-47)
🪛 Ruff (0.14.10)
Server/src/transport/unity_transport.py

102-102: Do not catch blind exception: Exception

(BLE001)

Server/src/transport/plugin_hub.py

70-70: Do not catch blind exception: Exception

(BLE001)


84-84: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)


87-87: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)


89-89: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)


177-177: Do not catch blind exception: Exception

(BLE001)


207-207: Consider moving this statement to an else block

(TRY300)


474-474: Do not catch blind exception: Exception

(BLE001)


482-482: Do not catch blind exception: Exception

(BLE001)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Sourcery review
🔇 Additional comments (25)
MCPForUnity/Editor/Tools/ReadConsole.cs (1)

155-160: LGTM! Robust guard against transient editor states.

This early-return check prevents tool calls from failing or hanging when the Unity editor is compiling or performing domain reloads—states where reflection APIs and console internals may be unstable. Returning a structured error with a "retry" hint allows clients to implement appropriate backoff logic rather than hard-failing.

The placement is correct: reflection members are validated first (lines 133–153), then editor state, then action processing. This aligns well with the PR's goal of introducing non-blocking CLI status checks and retry semantics.

MCPForUnity/Editor/Services/Transport/TransportCommandDispatcher.cs (6)

19-24: LGTM!

The [InitializeOnLoad] attribute ensures the static constructor runs on the main thread during editor initialization. Using int for _processingFlag is correct for Interlocked operations.


67-82: LGTM!

The static constructor correctly captures the main thread context at editor load time. The permanent update hook avoids "must focus Unity" edge cases when processing commands from background websocket threads.


110-116: LGTM!

Proactively waking the main thread after enqueueing improves responsiveness when the editor is unfocused or during domain reload transitions.


167-191: LGTM!

The nested Pump() function and context-aware dispatch are consistent with the pattern in RunOnMainThreadAsync. Best-effort semantics are appropriate for this wakeup mechanism.


204-217: LGTM!

Keeping the update hook permanently installed avoids edge cases where commands arrive while Unity is unfocused. The no-op implementations maintain backward compatibility with existing call sites.


219-260: LGTM!

The reentrancy guard using Interlocked.Exchange prevents concurrent ProcessQueue invocations. The try/finally ensures the flag resets correctly even on early returns. Lock scope is appropriately minimal—held only to snapshot ready commands.

MCPForUnity/Editor/Services/Transport/Transports/WebSocketTransportClient.cs (1)

69-74: LGTM!

Clean simplification by delegating to the centralized RunOnMainThreadAsync dispatcher. This removes the previous boilerplate (custom TCS, EditorApplication.delayCall, and manual registration handling) while preserving cancellation support and null-safety.

Server/src/transport/unity_transport.py (2)

12-12: LGTM: Import enables structured error responses.

The MCPResponse import is necessary for the fail-fast error handling below and aligns with the PR's goal of standardized error signaling across transports.


95-107: The core concern about validation errors receiving hint="retry" is incorrect.

The validation checks at lines 10–11 and 16–18 raise ValueError and TypeError before the try/except block begins at line 19. These exceptions propagate immediately and are not caught by the exception handler at line 26, so validation errors will not receive hint="retry".

The try/except block (lines 19–31) only wraps the PluginHub.send_command_for_instance() call and normalize_unity_response(), catching only failures from those operations. The broad Exception catch and hint="retry" response are appropriate for transient errors (e.g., network issues, Unity reloading) that may occur during command execution. This design is consistent with the existing preflight check in PluginHub.send_command_for_instance() that already returns the same structured retry hint when Unity is reloading.

Likely an incorrect or invalid review comment.

Server/src/transport/plugin_hub.py (5)

34-45: LGTM!

The new PluginDisconnectedError exception and _extract_project_hash helper are well-implemented. The use of rpartition is a safe way to handle the "Project@hash" format.


146-164: LGTM!

The fail-fast enhancement for disconnected sessions is well-implemented. Proactively failing in-flight commands with PluginDisconnectedError prevents callers from waiting until COMMAND_TIMEOUT when the plugin has already disconnected.


191-217: LGTM!

The enhanced error handling with structured retry hints for fast-fail commands is well-designed. Returning MCPResponse with hint="retry" instead of raising exceptions allows callers to handle transient failures gracefully.

The static analysis suggestion (TRY300) to move return result to an else block is a minor style preference that doesn't affect correctness here.


333-337: LGTM!

The update to handle the new _pending structure is correct. Accessing the entry outside the lock is safe since you're reading from an already-retrieved reference, and the isinstance check properly handles the case where the entry may have been removed.


453-499: LGTM!

The preflight and readiness probing logic is well-designed:

  1. Status file check - Avoids sending commands when Unity explicitly reports reloading
  2. Bounded readiness probe - Uses ping to verify Unity's main thread is responsive before issuing fast-fail commands
  3. Configurable timeout - Allows tuning via UNITY_MCP_SESSION_READY_WAIT_SECONDS with sensible bounds (0-30s)

The while-else construct is a clean way to handle the "deadline reached without successful ping" case, returning an appropriate retry hint.

Server/src/services/resources/editor_state.py (1)

45-51: Type safety concern at line 51 is unfounded; design intent is well-documented.

The code is correct as-is. The send_with_unity_instance function signature guarantees a return type of dict[str, Any] | MCPResponse, so the isinstance check at line 45 properly narrows the type. Line 51 safely returns non-dict responses (which must be MCPResponse), maintaining type safety.

Additionally, the extra guards at lines 46–49 are intentional: the comment at lines 42–44 explicitly explains that validation errors from malformed data are expected to bubble up. The code guards specific known failure cases (success=False, data=None) while allowing other structural mismatches to fail loudly—this is the documented design, not a gap. This pattern is defensive but lighter-weight than the simpler ternary used in other resources (e.g., windows.py).

Likely an incorrect or invalid review comment.

MCPForUnity/Editor/Windows/Components/Connection/McpConnectionSection.cs (9)

22-27: LGTM: Clean transport enum split.

The enum clearly distinguishes HTTP Local from HTTP Remote, aligning with the PR's first-class transport selection goals.


165-192: Fire-and-forget async stop is appropriate here.

The best-effort transport cleanup intentionally doesn't await to avoid blocking the UI. The ContinueWith error handler ensures exceptions are logged rather than left unobserved.


197-205: Good debouncing pattern for URL field.

Persisting and normalizing only on focus-out or Enter prevents the UI from fighting user input during typing.


264-332: LGTM: Clear connection status management.

The method appropriately toggles UI visibility based on transport mode and session state, with good separation between HTTP Local controls and manual session controls.


364-374: Good localhost validation for HTTP Local mode.

Clear messaging when the URL doesn't meet HTTP Local requirements, preventing user confusion.


438-445: Excellent polling throttle to avoid expensive checks.

The 0.75-second throttle for IsLocalHttpServerRunning() prevents excessive port/PID lookups from impacting editor responsiveness.


471-525: LGTM: Clean consolidated toggle with proper re-entrancy guard.

The unified start/stop flow handles session coordination correctly. The broad exception catch is appropriate for a UI handler that needs to display user-friendly error dialogs.


608-660: Good auto-start pattern with server readiness check.

Waiting for the HTTP server to accept connections before starting the session reduces race conditions and transient connection failures.


763-828: LGTM: Clean health verification with smart log deduplication.

The state tracking via lastHealthStatus prevents log spam while ensuring status changes are properly recorded.

Comment on lines +662 to +749
private static async Task<bool> WaitForHttpServerAcceptingConnectionsAsync(TimeSpan timeout)
{
try
{
string baseUrl = HttpEndpointUtility.GetBaseUrl();
if (!Uri.TryCreate(baseUrl, UriKind.Absolute, out var uri) || uri.Port <= 0)
{
return true; // Don't block if URL cannot be parsed
}

string host = uri.Host;
int port = uri.Port;

// Normalize wildcard/bind-all hosts to loopback for readiness checks.
// When the server binds to 0.0.0.0 or ::, clients typically connect via localhost/127.0.0.1.
string normalizedHost;
if (string.IsNullOrWhiteSpace(host)
|| string.Equals(host, "0.0.0.0", StringComparison.OrdinalIgnoreCase)
|| string.Equals(host, "::", StringComparison.OrdinalIgnoreCase)
|| string.Equals(host, "*", StringComparison.OrdinalIgnoreCase))
{
normalizedHost = "localhost";
}
else
{
normalizedHost = host;
}

var deadline = DateTime.UtcNow + timeout;
while (DateTime.UtcNow < deadline)
{
try
{
using var client = new TcpClient();
var connectTask = client.ConnectAsync(normalizedHost, port);
var completed = await Task.WhenAny(connectTask, Task.Delay(250));
if (completed != connectTask)
{
// Avoid leaving a long-running ConnectAsync in-flight (default OS connect timeout can be very long),
// which can accumulate across retries and impact overall editor/network responsiveness.
// Closing the client will cause ConnectAsync to complete quickly (typically with an exception),
// which we then attempt to observe (bounded) by awaiting.
try { client.Close(); } catch { }
}

try
{
// Even after Close(), some platforms may take a moment to complete the connect task.
// Keep this bounded so the readiness loop can't hang here.
var connectCompleted = await Task.WhenAny(connectTask, Task.Delay(250));
if (connectCompleted == connectTask)
{
await connectTask;
}
else
{
_ = connectTask.ContinueWith(
t => _ = t.Exception,
System.Threading.CancellationToken.None,
TaskContinuationOptions.OnlyOnFaulted,
TaskScheduler.Default);
}
}
catch
{
// Ignore connection exceptions and retry until timeout.
}

if (client.Connected)
{
return true;
}
}
catch
{
// Ignore and retry until timeout
}

await Task.Delay(150);
}

return false;
}
catch
{
return false;
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Complex async cancellation pattern may leak tasks under repeated retries.

The method attempts to work around long-lived TcpClient.ConnectAsync operations by manually closing the client (Line 704) and using fire-and-forget continuations (Lines 718-722) for tasks that don't complete within 250ms. While this is defensive, the fire-and-forget continuation only observes exceptions without actually cancelling the underlying operation, potentially leaving tasks in-flight if auto-start is triggered repeatedly (e.g., rapid server restarts during development).

Consider refactoring to use CancellationTokenSource explicitly with a passed cancellation token, which would provide cleaner cancellation semantics and avoid the double-timeout pattern.

🔎 Suggested refactor using CancellationToken
-        private static async Task<bool> WaitForHttpServerAcceptingConnectionsAsync(TimeSpan timeout)
+        private static async Task<bool> WaitForHttpServerAcceptingConnectionsAsync(TimeSpan timeout, System.Threading.CancellationToken cancellationToken = default)
         {
             try
             {
                 string baseUrl = HttpEndpointUtility.GetBaseUrl();
                 if (!Uri.TryCreate(baseUrl, UriKind.Absolute, out var uri) || uri.Port <= 0)
                 {
                     return true;
                 }

                 string host = uri.Host;
                 int port = uri.Port;

                 string normalizedHost;
                 if (string.IsNullOrWhiteSpace(host)
                     || string.Equals(host, "0.0.0.0", StringComparison.OrdinalIgnoreCase)
                     || string.Equals(host, "::", StringComparison.OrdinalIgnoreCase)
                     || string.Equals(host, "*", StringComparison.OrdinalIgnoreCase))
                 {
                     normalizedHost = "localhost";
                 }
                 else
                 {
                     normalizedHost = host;
                 }

                 var deadline = DateTime.UtcNow + timeout;
-                while (DateTime.UtcNow < deadline)
+                while (DateTime.UtcNow < deadline && !cancellationToken.IsCancellationRequested)
                 {
                     try
                     {
+                        using var connectCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
+                        connectCts.CancelAfter(250);
                         using var client = new TcpClient();
-                        var connectTask = client.ConnectAsync(normalizedHost, port);
-                        var completed = await Task.WhenAny(connectTask, Task.Delay(250));
-                        if (completed != connectTask)
-                        {
-                            try { client.Close(); } catch { }
-                        }
-
                         try
                         {
-                            var connectCompleted = await Task.WhenAny(connectTask, Task.Delay(250));
-                            if (connectCompleted == connectTask)
-                            {
-                                await connectTask;
-                            }
-                            else
-                            {
-                                _ = connectTask.ContinueWith(
-                                    t => _ = t.Exception,
-                                    System.Threading.CancellationToken.None,
-                                    TaskContinuationOptions.OnlyOnFaulted,
-                                    TaskScheduler.Default);
-                            }
+                            await client.ConnectAsync(normalizedHost, port, connectCts.Token);
+                            if (client.Connected)
+                            {
+                                return true;
+                            }
                         }
-                        catch
+                        catch (OperationCanceledException)
                         {
-                            // Ignore connection exceptions and retry until timeout.
+                            // Timeout or cancellation, retry
                         }
-
-                        if (client.Connected)
+                        catch
                         {
-                            return true;
+                            // Connection failed, retry
                         }
                     }
                     catch
                     {
-                        // Ignore and retry until timeout
+                        // Unexpected error, retry
                     }

                     await Task.Delay(150);
                 }

                 return false;
             }
             catch
             {
                 return false;
             }
         }

Note: This requires TcpClient.ConnectAsync overload that accepts CancellationToken, which was added in .NET 5. If targeting an older framework, the current pattern is acceptable with the noted limitations.

Note: The wildcard host normalization (Lines 675-688) correctly addresses the prior review concern about bind-all addresses like 0.0.0.0 and ::.

for the host normalization fix.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (4)
Server/src/transport/unity_transport.py (1)

95-109: Consider distinguishing retryable from non-retryable errors.

The error handling converts all exceptions to MCPResponse with hint="retry", but some exceptions (e.g., TypeError, ValueError for malformed parameters) represent non-transient errors that won't benefit from retrying. Consider categorizing exceptions to provide more accurate retry guidance.

Additionally, please verify that normalize_unity_response correctly handles both input types: raw Unity responses (success path, line 101) and MCPResponse.model_dump() dicts (error path, line 108).

🔎 Suggested categorization approach
         try:
             raw = await PluginHub.send_command_for_instance(
                 unity_instance,
                 command_type,
                 params,
             )
             return normalize_unity_response(raw)
         except Exception as exc:
             # NOTE: asyncio.TimeoutError has an empty str() by default, which is confusing for clients.
             err = str(exc) or f"{type(exc).__name__}"
-            # Fail fast with a retry hint instead of hanging for COMMAND_TIMEOUT.
-            # The client can decide whether retrying is appropriate for the command.
+            # Distinguish transient errors (worth retrying) from permanent errors.
+            is_retryable = isinstance(exc, (asyncio.TimeoutError, ConnectionError, OSError))
+            hint = "retry" if is_retryable else None
             return normalize_unity_response(
-                MCPResponse(success=False, error=err, hint="retry").model_dump()
+                MCPResponse(success=False, error=err, hint=hint).model_dump()
             )
Server/src/transport/plugin_hub.py (3)

47-47: Add ClassVar annotations to mutable class attributes.

The mutable class attributes _FAST_FAIL_COMMANDS, _connections, and _pending should be annotated with typing.ClassVar to clarify they are class-level shared state and improve type-checker understanding.

🔎 Proposed fix
+from typing import Any, ClassVar
-from typing import Any

 class PluginHub(WebSocketEndpoint):
     """Manages persistent WebSocket connections to Unity plugins."""
 
     encoding = "json"
     KEEP_ALIVE_INTERVAL = 15
     SERVER_TIMEOUT = 30
     COMMAND_TIMEOUT = 30
     # Fast-path commands should never block the client for long; return a retry hint instead.
     # This helps avoid the Cursor-side ~30s tool-call timeout when Unity is compiling/reloading
     # or is throttled while unfocused.
-    _FAST_FAIL_COMMANDS: set[str] = {"read_console", "get_editor_state", "ping"}
+    _FAST_FAIL_COMMANDS: ClassVar[set[str]] = {"read_console", "get_editor_state", "ping"}
 
     _registry: PluginRegistry | None = None
-    _connections: dict[str, WebSocket] = {}
+    _connections: ClassVar[dict[str, WebSocket]] = {}
     # command_id -> {"future": Future, "session_id": str}
-    _pending: dict[str, dict[str, Any]] = {}
+    _pending: ClassVar[dict[str, dict[str, Any]]] = {}

Also applies to: 50-50, 52-52


168-169: Consider logging when ignoring invalid timeout parameters.

When parsing the requested timeout fails, the exception is silently ignored. Adding a debug log would aid troubleshooting without changing behavior.

🔎 Proposed addition
                     # Give the server a small cushion beyond the Unity-side timeout to account for transport overhead.
                     server_wait_s = max(server_wait_s, requested_s + 5.0)
                 except Exception:
-                    pass
+                    logger.debug(f"Ignoring invalid timeout parameter: {requested}")

323-327: Consider acquiring lock when accessing _pending in _handle_command_result.

While CPython's GIL provides some protection, accessing cls._pending without the lock at line 323 could race with on_disconnect cleanup (lines 110-123) or the send_command finally block (line 210). Acquiring the lock would ensure consistency across Python implementations.

🔎 Proposed fix
     async def _handle_command_result(self, payload: CommandResultMessage) -> None:
         cls = type(self)
         lock = cls._lock
         if lock is None:
             return
         command_id = payload.id
         result = payload.result
 
         if not command_id:
             logger.warning(f"Command result missing id: {payload}")
             return
 
-        async with lock:
-            entry = cls._pending.get(command_id)
-        future = entry.get("future") if isinstance(entry, dict) else None
+        async with lock:
+            entry = cls._pending.get(command_id)
+            future = entry.get("future") if isinstance(entry, dict) else None
+            
         if future and not future.done():
             future.set_result(result)
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 02902f7 and 1794038.

📒 Files selected for processing (2)
  • Server/src/transport/plugin_hub.py
  • Server/src/transport/unity_transport.py
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-11-05T18:23:12.349Z
Learnt from: msanatan
Repo: CoplayDev/unity-mcp PR: 368
File: MCPForUnity/UnityMcpServer~/src/resources/menu_items.py:15-15
Timestamp: 2025-11-05T18:23:12.349Z
Learning: In Unity MCP, the `name` parameter in the `mcp_for_unity_resource` decorator is the external API name exposed to MCP clients (LLMs, AI agents). The command string passed to `async_send_command_with_retry` or `async_send_with_unity_instance` (e.g., "get_menu_items") is the internal command identifier that must match the C# side. These are decoupled, allowing external API naming to evolve independently of internal command routing.

Applied to files:

  • Server/src/transport/unity_transport.py
  • Server/src/transport/plugin_hub.py
📚 Learning: 2025-10-13T13:27:23.040Z
Learnt from: msanatan
Repo: CoplayDev/unity-mcp PR: 316
File: TestProjects/UnityMCPTests/Assets/Tests/EditMode/Resources.meta:1-8
Timestamp: 2025-10-13T13:27:23.040Z
Learning: UnityMcpBridge is a legacy project kept for backwards compatibility; MCPForUnity is the only active Unity plugin project. GUID collisions between UnityMcpBridge and MCPForUnity are acceptable.

Applied to files:

  • Server/src/transport/unity_transport.py
🧬 Code graph analysis (1)
Server/src/transport/plugin_hub.py (1)
Server/tests/integration/test_helpers.py (2)
  • model_dump (10-13)
  • error (46-47)
🪛 Ruff (0.14.10)
Server/src/transport/unity_transport.py

102-102: Do not catch blind exception: Exception

(BLE001)

Server/src/transport/plugin_hub.py

47-47: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)


50-50: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)


52-52: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)


145-145: Do not catch blind exception: Exception

(BLE001)


157-157: Do not catch blind exception: Exception

(BLE001)


168-169: try-except-pass detected, consider logging the exception

(S110)


168-168: Do not catch blind exception: Exception

(BLE001)


197-197: Consider moving this statement to an else block

(TRY300)


453-453: Do not catch blind exception: Exception

(BLE001)


461-461: Do not catch blind exception: Exception

(BLE001)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Sourcery review
🔇 Additional comments (2)
Server/src/transport/unity_transport.py (1)

12-12: LGTM!

The import is required for the new error handling pathway and follows standard patterns.

Server/src/transport/plugin_hub.py (1)

444-477: LGTM: Readiness probe improves fast-path command reliability.

The bounded readiness probe for fast-path commands is well-designed:

  • Correctly excludes "ping" to avoid recursion
  • Uses a configurable, capped timeout window
  • Returns structured retry responses instead of hanging
  • Provides a better experience during Unity domain reload/reconnect scenarios

@dsarno dsarno merged commit c2a6b0a into CoplayDev:main Jan 2, 2026
2 checks passed
@dsarno dsarno deleted the http-improvements branch January 4, 2026 22:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant